From fe9c0fde5b1a3005b50357f889dbb200b3939e7c Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 7 Oct 2026 12:47:12 -0400 Subject: [PATCH] docs: modernize typing guide for Python 3.11+ Drop pre-3.10 syntax, type comments, and `from __future__ import annotations` examples. Use `A | B` unions and note lazy annotations in 3.14. Assisted-by: ClaudeCode:claude-opus-5-5 --- docs/guides/typing.md | 91 ++++++++++--------------------------------- 1 file changed, 21 insertions(+), 70 deletions(-) diff --git a/docs/guides/typing.md b/docs/guides/typing.md index d6d577a3..ead3d94a 100644 --- a/docs/guides/typing.md +++ b/docs/guides/typing.md @@ -3,19 +3,15 @@ ## Basics The most exciting thing happening right now in Python development is static -typing. Since Python 3.0, we've had function annotations, and since 3.6, -variable annotations. In 3.5, we got a "typing" library, which provides tools to -describe types. This is what static type hints look like: +typing. This is what static type hints look like: ```python def f(x: int) -> int: return x * 5 ``` -This does nothing at runtime, except store the object. If you add -`from __future__ import annotations`, it doesn't even store the actual object, -just the string you type here, so then anything that can pass the Python parser -is allowed here. +This does nothing at runtime, except store the annotation. In Python 3.14+, +annotations are evaluated lazily, only when something asks for them. It is not useless though! For one, it helps the reader. Knowing the types expected really gives you a much better idea of what is going on and what you @@ -35,12 +31,10 @@ your types. ### Adding types -There are three ways to add types. +There are two ways to add types: -1. They can be inline as annotations. Best for Python 3 code, usually. -2. They can be in special "type comments". Originally designed for Python 2 - code, and still requires the proper imports. -3. They can be in a separate file with the same name but with a `.pyi` +1. They can be inline as annotations. This is usually best. +2. They can be in a separate file with the same name but with a `.pyi` extension. This is important for type stubs or for cases where you don't want to add imports or touch the original code. You can annotate compiled files or libraries you don't control this way. @@ -48,21 +42,19 @@ There are three ways to add types. If you have a library you don't control, you can add "type stubs" for it, then give the type checker your stubs directory. It will pull the types from your stubs. If you are writing code for a Raspberry Pi, for example, you could add -the stubs for -the Pi libraries, and then validate your code, without ever even installing the -Pi-only libraries! +the stubs for the Pi libraries, and then validate your code, without ever even +installing the Pi-only libraries! You do not have to add types for every object - most of the time, you just need it for parameters and returns from functions. When running the type checker, you can use `reveal_type(...)` to show the inferred type of any object, which is -like a print statement but at type-checking time, or `reveal_locals()` to see -all local types. +like a print statement but at type-checking time. Import it from `typing` if the +code also runs. ### Configuration By default, the type checker does as little as possible, so that you can add it -iteratively -to a code base. By default: +iteratively to a code base. By default: - All untyped variables and return values will be `Any`. - Code inside untyped functions is not checked _at all_. @@ -92,7 +84,7 @@ monitors the types of a variable, and "narrows" it when something restricts it. For example: ```python -x: Union[A, B] +x: A | B if isinstance(x, A): reveal_type(x) else: @@ -107,7 +99,7 @@ the code, just type checking it, so both sides of the if are checked. You can manually force type narrowing with assert: ```python -x: Union[A, B] +x: A | B assert isinstance(x, A) reveal_type(x) ``` @@ -171,52 +163,17 @@ special methods, like `Iterable`, `Iterator`, etc. Static typing has some great features worth checking out: -- Unions (New syntax in Python 3.10) -- Generic Types (New syntax in Python 3.9) +- Unions (`A | B`) +- Generic Types (like `list[A]`) - Literals - TypedDict -- Nicer NamedTuple definition (very popular in Python 3 code) +- Nicer NamedTuple definition - The type checker validates with the Python version you ask for, regardless of what version you are actually running. ## Complete example -### Runtime compatible types - -Here's the classic syntax, which you need to use if you want to access the type -annotations at runtime and you need to support Python < 3.10: - -```python -from typing import Union, List - - -# Generic types take bracket arguments -def f(x: int) -> List[int]: - return list(range(x)) - - -# Unions are a list of types that all could be allowed -def g(x: Union[str, int]) -> None: - # Type narrowing - Unions get narrowed - if isinstance(x, str): - print("string", x.lower()) - else: - print("int", x) - - # Calling x.lower() is invalid here! -``` - -### Types as strings - -If you don't access the types at runtime, or if you use Python 3.10+ only, then -you can use a much nicer syntax. The `annotations` future feature causes the -annotations to be stored as strings and not evaluated, which allows you to write -things that are not yet valid, like `list[int]`! - ```python -from __future__ import annotations - - def f(x: int) -> list[int]: return list(range(x)) @@ -228,13 +185,8 @@ def g(x: str | int) -> None: print("int", x) ``` -Notice that there are no imports from typing! Note that you cannot use the "new" -syntax in non annotation locations (like unions in `isinstance`) unless Python -supports it at runtime. And some libraries, like Typer and cattrs, use the -annotations at runtime. - -You can use the above in earlier Python versions if you use strings manually, -with the same caveats. +Notice that there are no imports from typing; you can sometimes avoid using it +entirely for simple cases. ## Tips for good types @@ -246,7 +198,6 @@ When you have a function, you should take as generic a type as possible, and return as specific a type as possible. For example: ```python -from __future__ import annotations from collections.abc import Iterable, Mapping @@ -285,12 +236,12 @@ through, usually using TypeVar (or the new generic syntax in Python 3.12, but that's not available for older Pythons via an import). Also note that the best place to get these in modern Python is -`collections.abc`, but if you need to subscript them at runtime, you'll need -Python 3.9+ or the versions in `typing`. +`collections.abc`; the ones in `typing` are deprecated if they have a working +alternative in `collections.abc`. ## Final words -When run alongside a good linter like flake8, this can catch a huge number of +When run alongside a good linter like Ruff, this can catch a huge number of issues before tests or they are discovered in the wild! It also prompts _better design_, because you are thinking about how types work and interact. It's also more readable, since if I give you code like this: