Skip to content
v0.1.0latest release21 skills2 categories3 sub-categories22 tested scripts68 reference docsContact us

Typing Cheatsheet by Python Version

Reference for python-type-hints-annotator · 359 lines

Programming / Pythonv1.0.0Updated OCT 20 20265 references1 script

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

PythonAdded to typing / syntaxPEP
3.5–3.8typing module, variable annotations (3.6), Literal, Final, Protocol, TypedDict (3.8)484, 526, 586, 591, 544, 589
3.9builtin generics list[int], collections.abc.X[...] subscriptable, Annotated585, 593
3.10X | Y at runtime, ParamSpec, Concatenate, TypeAlias, TypeGuard, is_typeddict604, 612, 613, 647
3.11Self, Never, LiteralString, Required/NotRequired, TypeVarTuple, Unpack, assert_never, assert_type, reveal_type, dataclass_transform, generic TypedDict/NamedTuple673, 655, 675, 646, 681
3.12type X = ..., def f[T](), class C[T]:, @override, **kwargs: Unpack[TD], TypeAliasType695, 698, 692
3.13TypeIs, ReadOnly, type parameter defaults, warnings.deprecated, NoDefault742, 705, 696, 702
3.14annotations evaluated lazily by default, annotationlib649, 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

python
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
TypeMeaning
int, float, complexnumeric tower: int is accepted where float is expected, float/int where complex is
boolsubtype of int
str, bytestext vs binary; bytes also accepts bytearray/memoryview unless you enable mypy strict_bytes / pyright disableBytesTypePromotions (PEP 688)
Nonein annotations means type(None)
objectany value, but only object methods may be used
Anyany value, every operation allowed: turns checking off
type[C]the class C or a subclass, not an instance

Containers and abstract collections

python
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
ConcreteAbstract (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 objectIterator[T], Generator[Y, S, R]collections.abc
re.Pattern[str], re.Match[str]re
context managercontextlib.AbstractContextManager[T]contextlib

Notes:

  • tuple[()] is the empty tuple; tuple[int, ...] is "any length of ints".
  • list and dict are invariant: list[bool] is not a list[int]. Read-only Sequence and Mapping (values) are covariant, so accept those.
  • str is a Sequence[str]: a function taking Sequence[str] will happily accept a single string. If that is a bug risk, accept list[str] | tuple[str, ...].
  • Mapping keys are invariant; Mapping[str, float] does not accept dict[str | int, float].

Optional and unions

python
# 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.9
  • Optional[X] means "X or None", not "optional argument". An argument with a default is optional regardless of its type.
  • X | None works in isinstance(x, int | str) from 3.10.
  • Order in a union does not matter to checkers; put None last for readability.

Callables

python
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:

python
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

python
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 type

Checkers differ on literal inference for plain variables, so annotate constants that feed Literal parameters with Final or an explicit Literal[...] type.

  • Literal accepts int, str, bytes, bool, enum members and None; 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.
  • Final in a dataclass field means a frozen per-instance value; at class level without dataclass, it is a constant.

Type aliases

python
# 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 type alias is a TypeAliasType object at runtime: you cannot call it or use it in isinstance. Use JSON.__value__ to get the value.
  • NewType creates 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

python
# 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

python
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)
UseWhen
TypedDictdata is and stays a dict (JSON from/to an API, SDK kwargs); zero runtime cost, no validation
NamedTuplesmall immutable records that should unpack like tuples
dataclassinternal objects with behaviour, defaults, equality
pydantic BaseModeluntrusted input that must be validated / coerced; JSON Schema generation

Async types

python
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
AnnotationUse 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

AnnotationMeaning
-> Nonereturns nothing (always write it, including __init__)
-> NoReturn / -> Nevernever returns normally (always raises, sys.exit, infinite loop). Never is 3.11 and also usable as a parameter type ("cannot be called")
-> Selfinstance 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

ModeWhen annotations are evaluatedEffect
Default 3.9–3.13at def/class creationlist[int] | None fails on 3.9; forward refs need quotes
from __future__ import annotationsnever (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
python
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

OldNewMinimum
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, ChainMapcollections.deque, defaultdict, ...3.9
typing.Callable, Iterable, Iterator, Generator, Sequence, Mapping, Awaitable, AsyncIterator, ...collections.abc.*3.9
typing.ContextManager, AsyncContextManagercontextlib.AbstractContextManager, AbstractAsyncContextManager3.9
typing.Pattern, typing.Matchre.Pattern, re.Match3.9
typing.Textstrany
Optional[X]X | None3.10
Union[A, B]A | B3.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.Selfsee matrix
T = TypeVar("T", default=str) (typing_extensions)[T = str]3.13
typing_extensions.deprecatedwarnings.deprecated3.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.

More in Python

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.