Skip to content

Commit 7c39222

Browse files
committed
add PEP-649 in Python type hint
1 parent 62f2d36 commit 7c39222

File tree

1 file changed

+93
-5
lines changed

1 file changed

+93
-5
lines changed

docs/posts/2025/2025-02-01-python-type-hints.md

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,45 @@ class C2:
231231

232232
```
233233

234-
## Postponed Evaluation of Annotations
234+
## Forward references
235235

236-
[PEP 563 (Postponed Evaluation of Annotations)](https://peps.python.org/pep-0563/) (also known as Future annotations import) allows you to use `from __future__ import annotations` to defer evaluation of type annotations until they're actually needed. Generally speaking, it turns every annotation into a string. This helps with:
236+
```python title="Forward reference (in type annotations)"
237+
def foo(x: MyType): # MyType not yet defined
238+
...
239+
class MyType:
240+
...
241+
242+
# NameError: name 'MyType' is not defined
243+
```
244+
245+
See [how Pydantic solves the forward reference problem](https://docs.pydantic.dev/latest/concepts/forward_annotations/).
246+
247+
## Circular references (import cycles)
248+
249+
See below [resolve import cycles](#resolve-import-cycles-by-pep-563) section for more details.
250+
251+
```python title="Circular reference (in object or module relationships)"
252+
class A:
253+
def __init__(self, b: B): # refers to B
254+
self.b = b
237255

238-
- [Forward references](https://docs.pydantic.dev/latest/concepts/forward_annotations/)
239-
- [Circular imports](#import-cycles)
256+
class B:
257+
def __init__(self, a: A): # refers to A
258+
self.a = a
259+
# NameError: name 'B' is not defined
260+
```
261+
262+
See [how Pydantic solves the circular reference problem](https://docs.pydantic.dev/latest/concepts/forward_annotations/#cyclic-references).
263+
264+
!!! note "A circular reference is a two-way (or multi-way) chain of forward references."
265+
266+
## PEP-563 Postponed Evaluation of Annotations
267+
268+
[PEP 563 (Postponed Evaluation of Annotations)](https://peps.python.org/pep-0563/) (also known as Future annotations import or stringized annotations) allows you to use `from __future__ import annotations` to defer evaluation of type annotations until they're actually needed.
269+
Generally speaking, it turns every annotation into a string. This helps with:
270+
271+
- [Forward references](#forward-references)
272+
- [Circular imports](#circular-references-import-cycles)
240273
- Performance improvements
241274

242275
`from __future__ import annotations` **must be the first executable line** in the file. You can only have shebang and comment lines before it.
@@ -259,14 +292,69 @@ user = User(name="Alice", age=30, friends=[])
259292
Future annotations import [doesn't support Python3.10 new syntax for union type](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-x-y-syntax-for-unions) (e.g., `int | str`), and it also doesn't support the new syntax for type variables with upper bounds (e.g., `type[C]`), neither for some dynamic evaluation of annotations.
260293
So it's preferable **NOT TO USE** `from __future__ import annotation` as much as possible, just use `string literal annotations` for forward references and circular imports.
261294

262-
## Import cycles
295+
## PEP-649 Deferred Evaluation of Annotations Using Descriptors
296+
297+
[PEP-563 (stringized annotations)](#pep-563-postponed-evaluation-of-annotations) solved the forward-reference and circular-reference problems for static type analysis users, and also fostered intriguing new uses for annotation metadata. But stringized annotations in turn caused chronic ==problems for runtime users of annotations==. (See why [PEP-563 is pain for runtime users like Pydantic](https://docs.pydantic.dev/latest/internals/resolving_annotations/).).
298+
299+
PEP-649 (String Literal Annotations) is proposed to be added in Python 3.14, it adds a new internal mechanism for ==lazily computing annotations on demand==, via a new object method called `__annotate__`.
300+
`self.__annotate__()` is called the first time (so called **lazy**) `self.__annotations__` attribute is accessed, and return value is stored in `self.__annotations__` and the result is cached for future accesses.
301+
This allows annotations to be computed only when needed, and also allows them to be computed in a way that can handle forward references and circular references.
302+
303+
A high-level overview of the mechanism is as follows:
304+
305+
```python
306+
# https://peps.python.org/pep-0649/#comparison-of-annotation-semantics
307+
class function:
308+
# __annotations__ on a function object is already a
309+
# "data descriptor" in Python, we're just changing
310+
# what it does
311+
@property
312+
def __annotations__(self):
313+
return self.__annotate__()
314+
315+
# ...
316+
317+
def annotate_foo():
318+
return {'x': int, 'y': MyType, 'return': float}
319+
320+
def foo(x = 3, y = "abc"):
321+
...
322+
323+
foo.__annotate__ = annotate_foo
324+
325+
class MyType:
326+
...
327+
328+
foo_y_annotation = foo.__annotations__['y']
329+
```
330+
331+
> The important change is that the code constructing the annotations dict now lives in a function here, called `annotate_foo()`. But this function isn't called until we ask for the value of `foo.__annotations__`, and we don't do that until after the definition of `MyType`. So this code also runs successfully, and `foo_y_annotation` now has the correct value. The class `MyType` even though `MyType` wasn't defined until after the annotation was defined.
332+
333+
!!! note "The basic idea of PEP-649 was briefly discussed and rejected during the early-days of PEP-563 discussion."
334+
https://peps.python.org/pep-0649/#mistaken-rejection-of-this-approach-in-november-2017
335+
336+
## Resolve import cycles by PEP-563
337+
338+
!!! note "PEP-649 (Deferred Evaluation of Annotations Using Descriptors) is proposed to be added in Python 3.14"
263339

264340
[From MyPy](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#import-cycles): If the cycle import is only needed for type annotations:
265341

266342
```python title="File foo.py" hl_lines="3-6"
267343
from typing import TYPE_CHECKING
268344

269345
if TYPE_CHECKING:
346+
# https://peps.python.org/pep-0649/#static-typing-users
347+
# Static typing users (mypy, pyright, etc.) often combine PEP 563 with the
348+
# if typing.TYPE_CHECKING idiom to prevent their type hints from being loaded at runtime.
349+
# With PEP-649 (probably in Python 3.14), static typing users will probably
350+
# prefer FORWARDREF or SOURCE format.
351+
352+
# https://peps.python.org/pep-0649/#runtime-annotation-users
353+
# the usage of if typing.TYPE_CHECKING is not compatible with
354+
# runtime annotation users (e.g., FastAPI, Pydantic, etc).
355+
# With PEP-649 (probably in Python 3.14), Runtime annotation users will most likely
356+
# prefer VALUE format, though some (e.g. if they evaluate annotations eagerly in a decorator
357+
# and want to support forward references) may also use FORWARDREF format.
270358
import bar
271359

272360
def listify(arg: 'bar.BarClass') -> 'list[bar.BarClass]':

0 commit comments

Comments
 (0)