Search code examples
pythonmypypython-typingpython-3.9

How to define a generic type that is not a class?


I'd like to define a generic type. Something like:

from typing import TypeVar, Sequence, Union, Generic

T = TypeVar('T')
RecurSeqOf = Sequence[Union[Generic[T], Sequence[T]]]
# mypy error: Variable "typing.Generic" is not valid as a type

Is there a way to do it?

The whole background

Actually, I need a recursive generic type like

T = TypeVar('T')
RecurSeqOf = Sequence[Union[T, 'RecurSeqOf']]]

But the definition of recursive types is not yet supported by mypy.

That's why I work around this issue by making nested type definitions up to a limited depth (say, usually 5-6 levels, but in the example below two levels only for the sake of conciseness). Hence, all the more the need to shorten the pattern because I need to use it for different parameter types:

from typing import Sequence, Union, TypeVar, Generic


class A:
    pass


class B:
    pass


# RecurSeqOfA = Sequence[Union[A, 'RecurSeqOfA']]  # mypy error: Cannot resolve name "RecurSeqOfA" (possible cyclic definition)
RecurSeqOfA = Sequence[Union[A, Sequence[Union[A, Sequence[A]]]]]

# RecurSeqOfA = Sequence[Union[A, 'RecurSeqOfA']]  # mypy error: Cannot resolve name "RecurSeqOfA" (possible cyclic definition)
RecurSeqOfB = Sequence[Union[B, Sequence[Union[B, Sequence[B]]]]]

T = TypeVar('T')
# RecurSeqOf = Sequence[Union[Generic[T], 'RecurSeqOf']]  # error: Cannot resolve name "RecurSeqOf" (possible cyclic definition)
# additionally: error: Variable "typing.Generic" is not valid as a type
RecurSeqOf = Sequence[Union[Generic[T], Sequence[Generic[T]]]]  # error: Variable "typing.Generic" is not valid as a type

As suggested by the comment of MisterMiyagi:

from typing import TypeVar, MutableSequence

T = TypeVar('T', bound='RecurSeqOf')
RecurSeqOf = MutableSequence[T]

a: RecurSeqOf[str] = []
a.append("abc")
a.append([])  # mypy error: error: Argument 1 to "append" of "MutableSequence" has incompatible type "List[<nothing>]"; expected "str"
b: RecurSeqOf[str] = []
a.append(b)  # mypy error: Argument 1 to "append" of "MutableSequence" has incompatible type "MutableSequence[str]"; expected "str"
a.append(["cde"])  # mypy error: Argument 1 to "append" of "MutableSequence" has incompatible type "List[str]"; expected "str"

The definition itself is accepted by mypy. But it does not have the desired effect.


Solution

  • Since Sequence is already generic, one can directly use a type variable:

    from typing import TypeVar, Sequence, Union
    
    T = TypeVar('T')
    # [T, ...] | [[T, ...], ...]
    RecurSeqOf = Sequence[Union[T, Sequence[T]]]
    # T | [T, ...] | [[T, ...], ...]
    RecurSeqOfUnion = Union[RecurSeqOf[T], T]
    

    This is what the documentation calls a "User defined generic type alias". RecurSeqOf = ... defines an alias, and Sequence[Union[T, Sequence[T]]] is generic.


    This allows to define recursive types of fixed but arbitrary depth:

    a0: RecurSeqOf[int]
    a1: RecurSeqOf[RecurSeqOfUnion[int]]
    a2: RecurSeqOf[RecurSeqOfUnion[RecurSeqOfUnion[int]]]
    reveal_type(a0)  # typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]]
    reveal_type(a1)  # typing.Sequence[Union[typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]], builtins.int, typing.Sequence[Union[typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]], builtins.int]]]]
    reveal_type(a2)  # typing.Sequence[Union[typing.Sequence[Union[typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]], builtins.int, typing.Sequence[Union[typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]], builtins.int]]]], typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]], builtins.int, typing.Sequence[Union[typing.Sequence[Union[typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]], builtins.int, typing.Sequence[Union[typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]], builtins.int]]]], typing.Sequence[Union[builtins.int, typing.Sequence[builtins.int]]], builtins.int]]]]