Skip to content

Making a class generic in a stub file #60

Closed

Description

@gvanrossum

Mark Shannon came up with an example I don't know how to handle. Support we were to stubify collections.UserDict. This is a container class, its implementation inherits from collections.abc.MutableMapping. Let's assume we don't want to modify the implementation at all. In the stub file (collections.pyi) we go like this:

from typing import MutableMapping
class UserDict(MutableMapping[KT, VT]):
    ...

Now let's say we have some application code that uses UserDict and we want to annotate this:

from collections.abc import UserDict
def something(x: UserDict[int, str]):
    ...

Unfortunately, this code doesn't actually work, because the implementation UserDict class isn't generic (since collections.abc.MutableMapping isn't). But to a type checker this is what you're supposed to write -- the typechecker sees the UserDict from the stub, which is generic.

I'm stumped. Mark's suggestion is to have a dedicated function in the typing module (he calls it Type()) that could be used, e.g.:

from typing import Type
from collections.abc import UserDict
def something(x:Type(UserDict)[int, str]):
    ...

Or perhaps Type() should take an additional parameter to indicate how many type parameters it takes, e.g. Type(UserDict, 2).

Activity

  1. JukkaL commented on Mar 20, 2015

    @JukkaL
    Contributor

    I've come across this same issue. This is actually a common thing and applies to list vs. List as well. Note that this is a related to the representation of types such as typing.Pattern[AnyStr] but a bit different.

    Currently you can use string literal types to encode this information, though this is not yet enforced by mypy (mypy accepts an ordinary type expression UserDict[...], but it will fail at runtime):

    from collections.abc import UserDict
    def something(x: 'UserDict[int, str]') -> ...:
        ...
    

    We could tag some generic types in stubs as requiring string literal escaping if using <type>[...].

    Another potential alternative is to create a wrapper type such as typing.collections.abc.UserDict that supports indexing but also supports everything that UserDict does. Not sure if that's even possible to implement.

    A final option would be to have two UserDict classes, one for use in a type annotation and the other for use in other contexts (e.g., constructing an instance), a bit like Mark's suggestion (and like List vs list). For example:

    from collections.abc import UserDict
    from typing.collections.abc import UserDictType
    def f() -> UserDictType[int, str]:
        return UserDict(...)
    
  2. gvanrossum commented on Mar 23, 2015

    @gvanrossum
    MemberAuthor

    I like the solution of using string literals when this comes up.

  3. JukkaL commented on Apr 14, 2015

    @JukkaL
    Contributor

    Me, Mark and Andrey agreed that using string literals is the way to go. Stubs can be an exception (no need to ever use string literals in stubs).

  4. added
    resolution: wontfixA valid issue that most likely won't be resolved for reasons described in the issue
    on May 18, 2015
  5. e-kosten commented on Mar 24, 2021

    @e-kosten

    I see this has been closed for some time and that we can use string literals to annotate function parameters, but what about for the key and value type of UserDict subclasses?

    For example, suppose we have the simple class

    class ExampleDict(UserDict[str, str]):
        def upper_keys(self) -> Iterable[str]:
            for key in self.keys():
                yield key.upper()

    If we try to do

    example = ExampleDict({1: "bar"})

    MyPy correctly warns that

    error: Dict entry 0 has incompatible type "int": "str"; expected "str": "str"
    

    However, at runtime, this fails and we instead need to define the class as

    class ExampleDict(UserDict):
        def upper_keys(self) -> Iterable[str]:
            for key in self.keys():
                yield key.upper()

    in which case MyPy fails to identify any problems with

    example = ExampleDict({1: "bar"})
  6. ssbarnea commented on Jun 28, 2021

    @ssbarnea
    Sponsor

    What is the correct way subclass UserDict and keep mypy happy? At this moment

    class Foo(UserDict):
        pass
    

    gives:

     error: Missing type parameters for generic type "UserDict"
    
  7. JelleZijlstra commented on Jun 28, 2021

    @JelleZijlstra
    Member

    You need to use if TYPE_CHECKING, unfortunately. Something like:

    from typing import TYPE_CHECKING
    if TYPE_CHECKING:
        _Base = UserDict[A, B]
    else:
        _Base = UserDict
    class Foo(_Base): ...
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    resolution: wontfixA valid issue that most likely won't be resolved for reasons described in the issue

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions