The habit

If you've written typed Python for more than a few years, you've probably typed this line without thinking about it:

from __future__ import annotations

It goes at the top of the file, before anything else. Linters suggest it. Style guides recommend it. Cookiecutter templates include it by default. Most people who use it, myself included for a long time, carry a rough mental model: "it stops type hints from breaking when a class refers to itself." You add it, the errors go away, you move on.

I recently went back to verify that mental model directly, against the newest Python release, rather than assuming a decade-old explanation still holds. It doesn't, not in the way I expected. The real story turned out to be more specific, and more interesting, than "it prevents an error."

The problem it was built to solve

Here's the classic case. A linked-list node whose method returns another instance of the same class:

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

    def append(self, value) -> Node:   # Node, referenced inside Node's own body
        self.next = Node(value)
        return self.next

At the exact moment Python reads -> Node, is Node actually a usable name yet? The class statement hasn't finished executing. Python is still in the middle of building it. Historically (Python 3.9 and earlier, with no workaround applied), this really did raise NameError: name 'Node' is not defined, immediately, at class-definition time. The fix people reached for was either hand-quoting the annotation as a string (-> "Node") or, from Python 3.7 onward, adding from __future__ import annotations once at the top of the file. That import quietly turns every annotation in that file into a string automatically, deferring the problem instead of hitting it head-on.

That's the story that got passed down. It's also, as of Python 3.14, no longer the whole picture.

Testing it directly

I ran the exact snippet above on Python 3.14, no __future__ import, nothing quoted:

>>> n = Node(1)
>>> second = n.append(2)
>>> second.value
2
>>> Node.append.__annotations__
{'return': <class '__main__.Node'>}

It just works, and not in the "works because Python guessed" sense. __annotations__ shows a real, live Node class object as the return type, resolved correctly, with zero special import anywhere in the file.

That's a genuinely different result from what the textbook story predicts. Something changed.

What actually changed: PEP 649

Python 3.14 shipped PEP 649, which changes when annotations get evaluated, for every file, whether or not it opts in to anything. Before 3.14, annotations were computed eagerly, the instant the def or class statement ran. As of 3.14, they're computed lazily instead: attached as a small, auto-generated function (__annotate__) that only actually runs the first time something asks what the annotation is. By the time anything asks what append's return type is, Node the class already exists. The timing problem that used to bite is simply gone.

So does that make from __future__ import annotations obsolete? Not quite. The real remaining difference is worth being precise about, because it's not the one most people would guess.

The difference that's still there

I tested this with a name that never resolves, on purpose. Not a forward reference that eventually works: one that's genuinely missing.

Without the import:

def g(x: TrulyUndefinedType) -> int:
    return x

print("about to access __annotations__...")
print(g.__annotations__)
print("reached the end")

Running this as a real script:

about to access __annotations__...
NameError: name 'TrulyUndefinedType' is not defined

def g(...) itself succeeded, no error at definition time, exactly as PEP 649 predicts. But the moment something actually touches g.__annotations__, that lazy __annotate__ function runs for the first time, tries to resolve TrulyUndefinedType, and fails. "reached the end" never prints. The error didn't disappear. It moved, from definition time to first-access time.

With the import:

from __future__ import annotations

def h(x: TrulyUndefinedType) -> int:
    return x

print("about to access __annotations__...")
print(h.__annotations__)
print("reached the end")
about to access __annotations__...
{'x': 'TrulyUndefinedType', 'return': 'int'}
reached the end

Clean run, every time. With from __future__ import annotations, annotations are stored as plain strings, full stop. They're never evaluated as real expressions unless something explicitly asks. Accessing __annotations__ itself can never raise, regardless of what it references.

That's the actual, current-day value of the import: not "avoids a NameError somewhere in the program's lifetime" (Python 3.14 already does that by default, just later), but "guarantees that looking at __annotations__ is always completely safe."

Deferred is not resolved

One more boundary worth knowing: neither approach makes a genuinely missing name resolvable. If something explicitly asks Python to resolve the real type (typing.get_type_hints(), which type checkers and some validation libraries call under the hood), the same NameError shows up either way:

from __future__ import annotations
import typing

def h(x: TrulyUndefinedType) -> int:
    return x

typing.get_type_hints(h)
# NameError: name 'TrulyUndefinedType' is not defined

Deferring evaluation postpones the reckoning. It doesn't remove the requirement that the name eventually exist somewhere real.

Checking my own code against this

I went back to a small database-access module I'd written, the kind of file that opens with from __future__ import annotations out of habit:

from __future__ import annotations

import os
from functools import lru_cache
from pathlib import Path
from typing import Optional

It annotates with Engine, pd.DataFrame, and Optional. All three are already imported at the top of the file, before any function that uses them in a type hint is defined. None of its own annotations are actual forward references. The import isn't rescuing anything currently broken in that file.

That doesn't make it wrong to have there. It's cheap, it's consistent across every file in the same codebase, and it guarantees __annotations__ access stays safe even after a future edit introduces a real forward reference by accident. It's defensive style, not a patch for something broken today. Being able to tell the difference, by actually checking rather than assuming, is the more useful habit than the import itself.

The takeaway

from __future__ import annotations is still worth writing. But the reason changed under everyone's feet in Python 3.14, and the version most people carry around ("it stops a NameError from a self-referencing type hint") describes a problem the language now solves by default anyway. The real, current reason is narrower and more specific: it's the difference between an error that can surface the moment you inspect an object's annotations, and one that's guaranteed not to.

Worth testing your own assumptions against the actual, current behavior once in a while. Especially the ones you copy to the top of every file without a second thought.