pythonmypypython-typing

How to type annotate optional class type param


I have following code

from typing import TypeVar, Type, overload

T = TypeVar('T')


@overload
def foo(bar: Type[T]) -> T:
    ...

@overload
def foo(bar: Type[T] | None) -> T | None:
    ...


def foo(bar: Type[T] | None) -> T | None:
    # implementation goes here
    ...


class Bar:
    ...


bar = foo(Bar)
bar2 = foo(Bar | None)  # No overload variant of "foo" matches argument type "UnionType"

How to properly type hint case for bar2?


I tried some others:

Type[T | None], mypy says Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader

removing 2nd overload (resulting in only Type[T] allowed), mypy says No overload variant of "foo" matches argument type "UnionType" (meaning 2nd overload is incorrect for that case anyways)


Solution

  • Only concrete classes are assignable to type[T]:

    [...] the actual argument passed in at runtime must [...] be a concrete class object [...]

    Special types in annotations § type[] | Python typing spec

    (Admittedly, the spec isn't completely clear about this. However, the following paragraph holds true.)

    It is thus understood that an object of this type can be invoked to retrieve an instance of type T. type[T] | None means such a thing, or None; thus, bar might or might not be invocable.

    def foo(bar: type[T] | None) -> None:
        if bar is not None:
            reveal_type(bar)  # type[T]
            instance = bar()  # fine
    

    However, Bar | None returns an instance of UnionType at runtime, and objects of this kind cannot be invoked.

    foo(Bar | None)       # (Bar | None) is not None
                          # bar() => error
    

    What you want is the proposed TypeForm of PEP 747 (a draft PEP):

    def foo[T](bar: TypeForm[T]) -> T: ...
    
    reveal_type(foo(Bar | None))  # Bar | None