Does Python evaluate type hinting of a forward reference?

Consider the following code:

class Foo:
    def bar(self) -> Foo:
        return Foo()

This program will actually crash at runtime if you try running it with Python: when the interpreter sees the definition of bar, the definition of Foo is not yet finished. So, since Foo has not yet been added to the global namespace, we can’t use it as a type hint yet.

Similarly, consider this program:

class Foo:
    def bar(self) -> Bar:
        return Bar()

class Bar:
    def foo(self) -> Foo:
        return Foo()

This mutually dependent definition suffers from the same problem: while we’re evaluating Foo, Bar hasn’t been evaluated yet so the interpreter throws an exception.


There are three solutions to this problem. The first is to make some of your type hints strings, effectively “forward declaring” them:

class Foo:
    def bar(self) -> "Foo":
        return Foo()

This satisfies the Python interpreter, and won’t disrupt third party tools like mypy: they can just remove the quotes before parsing the type. The main disadvantage is that this syntax looks sort of ugly and clunky.

The second solution is to use type comments syntax:

class Foo:
    def bar(self):
        # type: () -> Foo
        return Foo()

This has the same benefits and disadvantages as the first solution: it satisfies the interpreter and tooling, but looks hacky and ugly. It also has the additional benefit that it keeps your code backwards-compatibile with Python 2.7.

The third solution is Python 3.7+ only — use the from __future__ import annotations directive:

from __future__ import annotations 

class Foo:
    def bar(self) -> Foo:
        return Foo()

This will automatically make all annotations be represented as strings. So we get the benefit of the first solution, but without the ugliness.

This behavior will eventually become the default in future versions of Python.

It also turns out that automatically making all annotations strings can come with some performance improvements. Constructing types like List[Dict[str, int]] can be surprisingly expensive: they’re just regular expressions at runtime and evaluated as if they were written as List.__getitem__(Dict.__getitem__((str, int)).

Evaluating this expression is somewhat expensive: we end up performing two method calls, constructing a tuple, and constructing two objects. This isn’t counting any additional work that happens in the __getitem__ methods themselves, of course — and the work that happens in those methods ends up being non-trivial out of necessity.

(In short, they need to construct special objects that ensure types like List[int] can’t be used in inappropriate ways at runtime — e.g. in isinstance checks and the like.)

Leave a Comment