What is the benefit of "from __future__ import annotations" if classes are imported for type hints anyway?

Question:

What is the benefit of importing from __future__ import annotations? When I understand it right I should stop unnecessary typing import in runtime.

In my example HelloWorld is only needed for typing. But with this code the output always is:

Should this happen?
x = World!

When I remove from hello import HelloWorld the typing help in PyCharm does not longer work (I can understand this, because it does not understand where HelloWorld is from).

from __future__ import annotations

from hello import HelloWorld

if __name__ == '__main__':
    def hello(x: str, hello: HelloWorld = None):
        if hello is not None:
            print('hello.data_01 =', hello.data_01)
        print('x =', x)


    hello('World!')

hello.py

from dataclasses import dataclass


@dataclass
class HelloWorld:
    data_01: str
    data_02: str


print("Should this happen?")

So my question is if I still need to do from hello import HelloWorld what benefits do I get from from __future__ import annotations?

Asked By: HennyKo

||

Answers:

The from __future__ import annotations import has one core advantage: it makes using forward references cleaner.

For example, consider this (currently broken) program.

# Error! MyClass has not been defined yet in the global scope
def foo(x: MyClass) -> None:
    pass

class MyClass:
    # Error! MyClass is not defined yet, in the class scope
    def return_copy(self) -> MyClass:
        pass

This program will actually crash when you try running it at runtime: you’ve tried using ‘MyClass’ before it’s actually ever defined. In order to fix this before, you had to either use the type-comment syntax or wrap each ‘MyClass’ in a string to create a forward reference:

def foo(x: "MyClass") -> None:
    pass

class MyClass:
    def return_copy(self) -> "MyClass":
        pass

Although this works, it feels very janky. Types should be types: we shouldn’t need to have to manually convert certain types into strings just to make types play nicely with the Python runtime.

We can fix this by including the from __future__ import annotations import: that line automatically makes all types a string at runtime. This lets us write code that looks like the first example without it crashing: since each type hint is actually a string at runtime, we’re no longer referencing something that doesn’t exist yet.

And typecheckers like mypy or Pycharm won’t care: to them, the type looks the same no matter how Python itself chooses to represent it.


One thing to note is that this import does not, by itself, let us avoid importing things. It simply makes it cleaner when doing so.

For example, consider the following:

from expensive_to_import_module import MyType

def blah(x: MyType) -> None: ...

If expensive_to_import_module does a lot of startup logic, that might mean it takes a non-negligible amount of time to import MyType. This won’t really make a difference once the program is actually running, but it does make the time-to-start slower. This can feel particularly bad if you’re trying to write short-lived command-line style programs: the act of adding type hints can sometimes make your program feel more sluggish when starting up.

We could fix this by making MyType a string while hiding the import behind an if TYPE_CHECKING guard, like so:

from typing import TYPE_CHECKING

# TYPE_CHECKING is False at runtime, but treated as True by type checkers.
# So the Python interpreters won't do the expensive import, but the type checker
# will still understand where MyType came from!
if TYPE_CHECKING:
    from expensive_to_import_module import MyType

# This is a string reference, so we don't attempt to actually use MyType
# at runtime.
def blah(x: "MyType") -> None: ...

This works, but again looks clunky. Why should we need to add quotes around the last type? The annotations future import makes the syntax for doing this a little smoother:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from expensive_to_import_module import MyType

# Hooray, no more quotes!
def blah(x: MyType) -> None: ...

Depending on your point-of-view, this may not seem like a huge win. But it does help make using type hints much more ergonomic, makes them feel more “integrated” into Python, and (once these become enabled by default) removes a common stumbling block newcomers to PEP 484 tend to trip over.

Answered By: Michael0x2a