Setting TypeVar upper bound to class defined afterwards

1.1k views Asked by At

I was looking through the typeshed source and saw that in the pathlib.pyi it does the following:

_P = TypeVar('_P', bound=PurePath)

...

class PurePath(_PurePathBase): ...

I have a similar case with a base class that returns a subclass from __new__ (similar to Path), so the type annotations would therefore be similar as well. However, defining the bound keyword to the class that is defined below it resolves to an NameError since the name has not yet been resolved (as I would've expected; trying due to typeshed source).

from abc import ABC
from typing import Type
from typing import TypeVar
from foo.interface import SomeInterface

_MT = TypeVar('_MT', bound=MyBaseClass)

class MyBaseClass(SomeInterface, ABC):
    def __new__(cls: Type[_MT], var: int = 0) -> _MT:
        if var == 1:
            return object.__new__(FirstSubClass)
        return object.__new__(cls)

class FirstSubClass(MyBaseClass): pass

How does typeshed get away with this? It would be perfect for my typing, otherwise I must do:

_MT = TypeVar('_MT', covariant=True, bound=SomeInterface)

And all my linter warnings are satisfied ("expected type _MT, got object instead")...

Better matching case is the typing a factory method since I am using __new__ as a factory similar to how Path does it and as described here. Still, it would be nice to know how typeshed accomplishes this forward reference to bound without using a string.

1

There are 1 answers

0
Zecong Hu On

In runtime Python code, you can use string literal types in the bound attribute to create a forward reference:

>>> _MT = TypeVar('_MT', bound='MyBaseClass')
>>> _MT.__bound__
ForwardRef('MyBaseClass')

As for why a non-string can be used in typeshed, it's because typeshed provides .pyi stub files, which are syntactically valid Python code, but are not meant to be executed, only to be examined by type checkers. There's little I could find on specifications for stub files, but it makes sense to assume that everything is implicitly a string literal. This seems to be implied from the mypy docs:

String literal types are never needed in # type: comments and stub files.