pythongenericspython-typing

How to specialize a class in Python (typing generics)


How to provide a different definition for a specialized (in typing sense) class?

Example, why that might be useful:

TElement = TypeVar('TElement')

class BaseCollection(Generic[TElement]):
    name: str
    data: List[TElement]

    def __add__(self, other: 'BaseCollection[TElement]'):
        return CombinedCollection[TElement].from_collections(self, other)
    ...

class Collection(BaseCollection):
    pass

# how do I do this specialization
class Collection[int](BaseCollection[int]):
    def sum(self):
        return sum(self.data)


# so that CombinedCollection[int] has also the sum method
class CombinedCollection(Collection[TElement]):
    @classmethod
    def from_collections(cls, *lists: Collection[TElement]):

        return CombinedCollection[TElement]('(' + '+'.join(l.name for l in lists) + ')', 
            [x for l in lists for x in l])

# i.e. I can do
c = Collection[int]('my_collection c', [1,2])
d = Collection[int]('my_collection d', [-1, -2, -3])

cd = c + d
# and now I can do this:
cd.sum()
# -3
cd.name
# (my_collection c+my_collection d)

Solution

  • There's actually a way to do this, but instead of creating "specializations" of a class, you need to define all methods of a class in the class definition (which is what Python would look like, if without type annotations), and add a type constraint on the self parameter of the method:

    class Collection(Generic[T]):
        def sum(self: "Collection[int]") -> int:
            ...
    
    a = C[int]([1, 2, 3])
    reveal_type(a.sum())  # int
    b = C[str](["a", "b", "c"])
    reveal_type(b.sum())  # error: Invalid self argument "C[str]" to attribute function "sum"
    

    See a concrete example on mypy-play.

    Note that this only works with concrete types and not type varaibles. If you replace int with a TypeVar with bounds (TypeVar("T", bound=numbers.Real)) or with constraints (TypeVar("T", int, float)), mypy seems to ignore them and will accept any type. This is also demonstrated in the example above. I believe this is a bug or an oversight.