Every construct you are likely to write, the version that introduced it, and the
fallback for older targets. Rule of thumb: write the newest syntax your
minimum supported Python runs, and take anything newer from
typing_extensions (it re-exports typing names when they exist, so the
import keeps working after you upgrade).
Contents
- Version matrix
- Basics
- Containers and abstract collections
- Optional and unions
- Callables
- Literal, Final, ClassVar, enums
- Type aliases
- Generics: TypeVar and PEP 695
- TypedDict, NamedTuple, dataclasses
- Async types
- Special return types
- Annotations at runtime: future import, TYPE_CHECKING, 3.14
- Old to new rewrite table
Version matrix
| Python | Added to typing / syntax | PEP |
|---|---|---|
| 3.5–3.8 | typing module, variable annotations (3.6), Literal, Final, Protocol, TypedDict (3.8) | 484, 526, 586, 591, 544, 589 |
| 3.9 | builtin generics list[int], collections.abc.X[...] subscriptable, Annotated | 585, 593 |
| 3.10 | X | Y at runtime, ParamSpec, Concatenate, TypeAlias, TypeGuard, is_typeddict | 604, 612, 613, 647 |
| 3.11 | Self, Never, LiteralString, Required/NotRequired, TypeVarTuple, Unpack, assert_never, assert_type, reveal_type, dataclass_transform, generic TypedDict/NamedTuple | 673, 655, 675, 646, 681 |
| 3.12 | type X = ..., def f[T](), class C[T]:, @override, **kwargs: Unpack[TD], TypeAliasType | 695, 698, 692 |
| 3.13 | TypeIs, ReadOnly, type parameter defaults, warnings.deprecated, NoDefault | 742, 705, 696, 702 |
| 3.14 | annotations evaluated lazily by default, annotationlib | 649, 749 |
Python 3.9 reached end of life in October 2025; new projects should target 3.10 or later, libraries often still support 3.9.
Basics
def greet(name: str, excited: bool = False) -> str:
return f"Hello {name}{'!' if excited else '.'}"
count: int = 0 # variable annotation (rarely needed: inference works)
hits: list[str] = [] # needed: empty container has no element type
model_name: str | None = None # needed: None alone would infer as None
class Settings:
api_key: str # declared, assigned in __init__
timeout_s: float = 30.0| Type | Meaning |
|---|---|
int, float, complex | numeric tower: int is accepted where float is expected, float/int where complex is |
bool | subtype of int |
str, bytes | text vs binary; bytes also accepts bytearray/memoryview unless you enable mypy strict_bytes / pyright disableBytesTypePromotions (PEP 688) |
None | in annotations means type(None) |
object | any value, but only object methods may be used |
Any | any value, every operation allowed: turns checking off |
type[C] | the class C or a subclass, not an instance |
Containers and abstract collections
from collections.abc import Iterable, Iterator, Mapping, MutableMapping, Sequence
def total_tokens(counts: Iterable[int]) -> int: # any iterable, single pass
return sum(counts)
def window(xs: Sequence[float], size: int) -> list[list[float]]: # needs len + indexing
return [list(xs[i:i + size]) for i in range(0, len(xs), size)]
def merge(base: Mapping[str, object], extra: Mapping[str, object]) -> dict[str, object]:
return {**base, **extra}
def bump(stats: MutableMapping[str, int], key: str) -> None: # mutates the argument
stats[key] = stats.get(key, 0) + 1| Concrete | Abstract (read) | Abstract (mutate) | Import (3.9+) |
|---|---|---|---|
list[T] | Sequence[T], Iterable[T], Collection[T] | MutableSequence[T] | collections.abc |
tuple[int, str] (fixed) / tuple[int, ...] (variable) | Sequence[T] | builtin | |
dict[K, V] | Mapping[K, V] | MutableMapping[K, V] | collections.abc |
set[T], frozenset[T] | AbstractSet[T] (from collections.abc import Set as AbstractSet) | MutableSet[T] | collections.abc |
collections.deque[T], defaultdict[K, V], OrderedDict[K, V], Counter[T], ChainMap[K, V] | collections | ||
| generator object | Iterator[T], Generator[Y, S, R] | collections.abc | |
re.Pattern[str], re.Match[str] | re | ||
| context manager | contextlib.AbstractContextManager[T] | contextlib |
Notes:
tuple[()]is the empty tuple;tuple[int, ...]is "any length of ints".listanddictare invariant:list[bool]is not alist[int]. Read-onlySequenceandMapping(values) are covariant, so accept those.stris aSequence[str]: a function takingSequence[str]will happily accept a single string. If that is a bug risk, acceptlist[str] | tuple[str, ...].Mappingkeys are invariant;Mapping[str, float]does not acceptdict[str | int, float].
Optional and unions
# 3.10+
def lookup(doc_id: str, cache: dict[str, str] | None = None) -> str | None: ...
# 3.9 with deferred annotations (annotations only, not runtime expressions)
from __future__ import annotations
def lookup(doc_id: str, cache: dict[str, str] | None = None) -> str | None: ...
# 3.9 without the future import
from typing import Optional, Union
def lookup(doc_id: str, cache: Optional[dict[str, str]] = None) -> Optional[str]: ...
Number = Union[int, float] # runtime alias: must use Union on 3.9Optional[X]means "X or None", not "optional argument". An argument with a default is optional regardless of its type.X | Noneworks inisinstance(x, int | str)from 3.10.- Order in a union does not matter to checkers; put
Nonelast for readability.
Callables
from collections.abc import Awaitable, Callable
OnToken = Callable[[str], None] # one str arg, returns None
Scorer = Callable[[str, str], float]
AnyArgs = Callable[..., str] # any arguments
AsyncHandler = Callable[[dict[str, object]], Awaitable[str]]
Factory = Callable[[], "Embedder"] # zero args (forward ref in a runtime alias)Callable cannot express keyword-only parameters, defaults or overloads. Use a
callback protocol:
from typing import Protocol
class Completion(Protocol):
def __call__(self, prompt: str, *, max_tokens: int = 256, temperature: float = 0.0) -> str: ...ParamSpec and Concatenate (decorators): see advanced-typing.md.
Literal, Final, ClassVar, enums
from enum import StrEnum # 3.11
from typing import ClassVar, Final, Literal
Provider = Literal["openai", "anthropic", "local"]
MAX_CONTEXT: Final = 128_000 # inferred Final[int]; reassignment is an error
class Role(StrEnum):
SYSTEM = "system"
USER = "user"
class Model:
registry: ClassVar[dict[str, type["Model"]]] = {} # class-level, not per instance
name: str # instance attribute
def set_mode(mode: Literal["chat", "completion"]) -> None: ...
set_mode("chat") # ok
m = "chat"
set_mode(m) # pyright: ok (narrowed to Literal["chat"]); mypy: error (m is inferred as str)
M: Final = "chat"
set_mode(M) # ok in both: Final keeps the literal typeCheckers differ on literal inference for plain variables, so annotate
constants that feed Literal parameters with Final or an explicit
Literal[...] type.
Literalacceptsint,str,bytes,bool, enum members andNone; not floats or expressions.LiteralString(3.11) accepts any string built only from literals: use it for SQL/prompt templates to block injection of user input into the template itself.Finalin a dataclass field means a frozen per-instance value; at class level without dataclass, it is a constant.
Type aliases
# 3.12+: lazy, supports forward references and type parameters
type JSON = dict[str, JSON] | list[JSON] | str | int | float | bool | None
type Pair[T] = tuple[T, T]
# 3.10–3.11: explicit alias
from typing import TypeAlias
JSON: TypeAlias = "dict[str, JSON] | list[JSON] | str | int | float | bool | None"
# 3.9: implicit alias (any module-level assignment of a type)
from typing import Dict, List, Union
JSON = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None]- A
typealias is aTypeAliasTypeobject at runtime: you cannot call it or use it inisinstance. UseJSON.__value__to get the value. NewTypecreates a distinct nominal type with zero runtime cost:DocId = NewType("DocId", str);DocId("a1")is required to make one, so ids cannot be mixed up with other strings.
Generics: TypeVar and PEP 695
# 3.12+
def first[T](items: Sequence[T]) -> T:
return items[0]
def longest[S: (str, bytes)](a: S, b: S) -> S: # constrained: S is exactly str or bytes
return a if len(a) >= len(b) else b
def top_k[N: float](scores: Sequence[N], k: int) -> list[N]: # bounded: any subtype of float
return sorted(scores, reverse=True)[:k]
class Cache[K, V]:
def __init__(self) -> None:
self._data: dict[K, V] = {}
def get(self, key: K) -> V | None:
return self._data.get(key)
# 3.13+: defaults
class Response[T = str]:
def __init__(self, body: T) -> None:
self.body = body
# 3.9–3.11
from typing import Generic, TypeVar
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
N = TypeVar("N", bound=float)
S = TypeVar("S", str, bytes)
class Cache(Generic[K, V]): ...PEP 695 parameters infer variance automatically; with old-style TypeVar you
declare covariant=True / contravariant=True yourself (needed for protocols).
TypedDict, NamedTuple, dataclasses
from dataclasses import dataclass, field
from typing import NamedTuple, NotRequired, ReadOnly, Required, TypedDict
class Usage(TypedDict):
prompt_tokens: int
completion_tokens: int
class Message(TypedDict, total=False): # every key optional...
role: Required[str] # ...except these (3.11)
content: str
class ToolCall(TypedDict):
id: ReadOnly[str] # 3.13: cannot be assigned through this type
name: str
arguments: str
index: NotRequired[int] # 3.11
class Hit(NamedTuple):
doc_id: str
score: float
@dataclass(frozen=True, slots=True) # slots=: 3.10
class Chunk:
text: str
source: str
tags: list[str] = field(default_factory=list)| Use | When |
|---|---|
TypedDict | data is and stays a dict (JSON from/to an API, SDK kwargs); zero runtime cost, no validation |
NamedTuple | small immutable records that should unpack like tuples |
dataclass | internal objects with behaviour, defaults, equality |
pydantic BaseModel | untrusted input that must be validated / coerced; JSON Schema generation |
Async types
from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator, Awaitable, Coroutine
async def complete(prompt: str) -> str: ... # annotate the awaited result
# complete is Callable[[str], Coroutine[Any, Any, str]]; calling it gives an awaitable
async def stream(prompt: str) -> AsyncIterator[str]: # async generator
for token in prompt.split():
yield token
async def consume(tokens: AsyncIterable[str]) -> str:
return "".join([t async for t in tokens])
def schedule(job: Awaitable[str]) -> None: ... # accepts coroutines, tasks, futures| Annotation | Use for |
|---|---|
Awaitable[T] | parameters that will be awaited (coroutine, Task, Future) |
Coroutine[Any, Any, T] | when you need .send / .close, or asyncio.create_task arguments (typeshed requires a coroutine there) |
AsyncIterator[T] | return of an async def with yield (simplest) |
AsyncGenerator[Y, S] | if callers use asend / aclose (2 params; current typeshed gives S a default of None, so AsyncGenerator[str] works in up-to-date checkers) |
AsyncIterable[T] | parameters consumed with async for |
asyncio.Task[T], asyncio.Future[T] | stored tasks |
contextlib.AbstractAsyncContextManager[T] | objects used with async with |
Special return types
| Annotation | Meaning |
|---|---|
-> None | returns nothing (always write it, including __init__) |
-> NoReturn / -> Never | never returns normally (always raises, sys.exit, infinite loop). Never is 3.11 and also usable as a parameter type ("cannot be called") |
-> Self | instance of the class the method was called on (builders, fluent APIs, classmethod constructors) |
-> Iterator[T] | generator |
-> TypeGuard[T] / -> TypeIs[T] | user-defined narrowing predicate |
Annotations at runtime
| Mode | When annotations are evaluated | Effect |
|---|---|---|
| Default 3.9–3.13 | at def/class creation | list[int] | None fails on 3.9; forward refs need quotes |
from __future__ import annotations | never (stored as strings) | new syntax OK in annotations on 3.9; no forward-ref quotes; get_type_hints() evaluates later |
| 3.14 default (PEP 649) | lazily, on first access of __annotations__ | forward refs work without quotes; annotationlib.get_annotations(obj, format=Format.FORWARDREF) for partial evaluation |
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from numpy.typing import NDArray # import only for the checker
import numpy as np
def normalize(v: NDArray[np.float32]) -> NDArray[np.float32]: ...Runtime consumers of annotations (pydantic, FastAPI, typer, attrs, SQLAlchemy,
typing.get_type_hints) must be able to resolve every name, so keep those
imports real and don't hide them under TYPE_CHECKING.
Old to new rewrite table
| Old | New | Minimum |
|---|---|---|
List[int], Dict[str, int], Set[str], FrozenSet[str], Tuple[int, ...], Type[C] | list[int], dict[str, int], set[str], frozenset[str], tuple[int, ...], type[C] | 3.9 |
typing.Deque, DefaultDict, OrderedDict, Counter, ChainMap | collections.deque, defaultdict, ... | 3.9 |
typing.Callable, Iterable, Iterator, Generator, Sequence, Mapping, Awaitable, AsyncIterator, ... | collections.abc.* | 3.9 |
typing.ContextManager, AsyncContextManager | contextlib.AbstractContextManager, AbstractAsyncContextManager | 3.9 |
typing.Pattern, typing.Match | re.Pattern, re.Match | 3.9 |
typing.Text | str | any |
Optional[X] | X | None | 3.10 |
Union[A, B] | A | B | 3.10 |
X: TypeAlias = ... | type X = ... | 3.12 |
T = TypeVar("T") + Generic[T] | class C[T]: / def f[T]() | 3.12 |
P = ParamSpec("P") | def deco[**P, R](fn: Callable[P, R]) | 3.12 |
Ts = TypeVarTuple("Ts") | class Array[*Ts]: | 3.12 |
TypeVar("T", bound=X) | [T: X] | 3.12 |
TypeVar("T", A, B) | [T: (A, B)] | 3.12 |
typing_extensions.Self etc. | typing.Self | see matrix |
T = TypeVar("T", default=str) (typing_extensions) | [T = str] | 3.13 |
typing_extensions.deprecated | warnings.deprecated | 3.13 |
**kwargs: Any with a known key set | **kwargs: Unpack[Options] (TypedDict) | 3.12 (3.11 via typing_extensions) |
scripts/annotation_audit.py --target-version X.Y reports each of these rewrites
with the exact replacement text and the import lines to change.
python-beginner-concept-tutor
Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.
python-comprehension-refactorer
Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.
python-context-manager-writer
Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.
python-dataclass-converter
Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON.
python-exception-handling-designer
Design and review Python exception handling: custom exception hierarchies with retryable flags, what to catch where, EAFP vs LBYL, chaining with raise ... from, else/finally, logger.exception, add_note, ExceptionGroup and except* with asyncio.TaskGroup, warnings, asserts vs validation, FastAPI error translation, retries with backoff and jitter, timeouts, cancellation, pytest.raises, plus LLM-app failures: 429 rate limits with retry-after, 5xx overloaded, context-length exceeded, content filters, broken streams, malformed JSON repair loops, tool-call errors fed back to the model, partial embedding batch failures, CUDA OOM batch halving, vector DB outages, circuit breakers and model fallbacks.
python-idiomatic-code-reviewer
Review Python code for non-idiomatic, unpythonic or risky patterns and rewrite it into clean modern Python: PEP 8 / PEP 20, EAFP vs LBYL, iteration idioms (enumerate, zip, comprehensions, any/all), unpacking, f-strings, pathlib, truthiness, `is None`, mutable defaults, walrus, match statements, collections / itertools / functools, logging instead of print, with-blocks and resource handling. Includes AI-engineering checks: hardcoded API keys, time.sleep or requests inside async def, missing HTTP timeouts, unbounded retry loops, model clients built at import time, prompt string concatenation, sequential awaits, eval() on LLM output. Ships a 46-rule AST reviewer with severities and suggested rewrites.