cheat sheet
typing-extensions
Package-level reference for typing-extensions on PyPI — what's backported, install, version policy, and migration to stdlib.
typing-extensions
What it is
typing-extensions is the backport package for typing features that are either too new to be in the stdlib of supported Python versions, or are still in PEP-draft / experimental status. It mirrors the typing module's API surface and adds new constructs (Self, TypeAlias, NotRequired, Required, Concatenate, ParamSpec, LiteralString, etc.) before they make it into the stdlib — usually one or two Python releases ahead.
Reach for typing-extensions when you want to: use Self on Python 3.10 (stdlib has it from 3.11); use TypedDict with NotRequired keys on 3.10 (stdlib gets it in 3.11); use Concatenate and ParamSpec on 3.9 (stdlib has them from 3.10); or use any PEP-655 / 695 / 696 / 698 feature on a Python that doesn't yet ship it.
Install
pip install typing-extensions
Output: (none — exits 0 on success; pure-Python, no runtime deps)
uv add typing-extensions
Output: dependency resolved + added to pyproject.toml
poetry add typing-extensions
Output: updated lockfile + virtualenv install
No optional extras — the package is a single pure-Python module.
Versioning & Python support
- Current line is the
4.xseries. Minor releases add new constructs and refine existing ones; major releases (rare) drop very-old Python versions. - Supports Python 3.8+ on recent releases; older Pythons can pin older
typing-extensions. - The library is forward-deflating — as constructs land in the stdlib, the package re-exports the stdlib version on newer Pythons and provides its own implementation on older ones. Your code can
from typing_extensions import Selfon any supported version and get the right thing.
Package metadata
- Maintainer: Python core developers (notably Guido van Rossum, Jelle Zijlstra)
- Project home: github.com/python/typing_extensions
- Docs: typing-extensions.readthedocs.io
- PyPI: pypi.org/project/typing-extensions
- License: PSF-2.0
- Governance: Part of the CPython project; tracks the
typingmodule's evolution - First released: 2017
- Downloads: consistently in PyPI top 5 (used by
pydantic,fastapi,mypy,starlette, every modern typed library)
Optional dependencies & extras
- None.
typing-extensionshas no third-party runtime dependencies.
Alternatives
| Package | Trade-off |
|---|---|
Standard library typing | Always preferred when the construct you need is available in your minimum supported Python. |
mypy_extensions | Older, niche backports for mypy-specific constructs (TypedDict, Arg). Mostly superseded by typing-extensions. |
Conditional sys.version_info guards | Manual workaround. Verbose, brittle. Use typing-extensions instead. |
typeguard | Different problem — runtime type enforcement. Pair with typing-extensions rather than replace. |
Common gotchas
from typing_extensions import Xworks on any Python version even whenXis already intyping— the backport re-exports the stdlib version. Use the import line that supports your oldest Python target.TypedDicthas two flavors — class-based (class TD(TypedDict): x: int) and functional (TD = TypedDict("TD", {"x": int})). The functional form is required for keys that aren't valid Python identifiers.NotRequiredandRequiredapply at the field level, not via separate base classes.class T(TypedDict): a: int; b: NotRequired[str].Selfwas added to stdlib in 3.11. Imports fromtyping_extensionscontinue to work but type checkers see them as the stdlib version when running under 3.11+.Annotatedhas been in stdlibtypingsince 3.9.typing_extensions.Annotatedis identical on 3.9+.assert_never/assert_typeare runtime-no-op helpers — they exist to direct type checkers, not enforce behavior at runtime.ConcatenateandParamSpecwork only in type checkers; runtime introspection of them is limited.
Real-world recipes
The recipes cover the constructs most frequently backported in 2026 codebases.
Recipe 1 — TypedDict with required and optional keys.
from typing_extensions import TypedDict, NotRequired, Required
class User(TypedDict):
id: int
name: str
email: NotRequired[str] # may be omitted
avatar_url: NotRequired[str]
# In TypedDict with total=False, every key is optional by default; use Required to invert
class PartialUser(TypedDict, total=False):
id: Required[int]
name: str
Output: type checkers (mypy / pyright) accept dicts that omit email; PartialUser requires id even when total=False. Runtime behavior is just a normal dict.
Recipe 2 — Annotated for parameter metadata.
from typing_extensions import Annotated
from dataclasses import dataclass
@dataclass
class Spec:
min: int
max: int
def clamp(x: int, bounds: Annotated[Spec, "default 0..100"] = Spec(0, 100)) -> int:
return max(bounds.min, min(bounds.max, x))
print(clamp(150))
Output:
100
Annotated[T, metadata] lets type checkers see T while frameworks (pydantic, FastAPI) read the metadata. Same syntax that pydantic uses for field constraints.
Recipe 3 — Self for fluent / builder APIs.
from typing_extensions import Self
class QueryBuilder:
def __init__(self) -> None:
self._clauses: list[str] = []
def where(self, clause: str) -> Self:
self._clauses.append(clause)
return self
def build(self) -> str:
return " AND ".join(self._clauses)
q = QueryBuilder().where("a=1").where("b=2").build()
print(q)
Output:
a=1 AND b=2
-> Self means subclasses are typed correctly too; MyBuilder().where(...) returns MyBuilder, not QueryBuilder.
Recipe 4 — Required / NotRequired for partial-update payloads.
from typing_extensions import TypedDict, NotRequired
class UpdateProfile(TypedDict):
user_id: int
name: NotRequired[str]
email: NotRequired[str]
bio: NotRequired[str]
def patch(data: UpdateProfile) -> None:
for k, v in data.items():
print(f"set {k} = {v!r}")
patch({"user_id": 42, "name": "Alice Dev"})
Output:
set user_id = 42
set name = 'Alice Dev'
Common in REST PATCH endpoints — TypedDict captures "send any subset, but user_id is required".
Recipe 5 — Concatenate for parameter-spec aware decorators.
from typing_extensions import ParamSpec, Concatenate
from typing import Callable, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def with_request_id(fn: Callable[Concatenate[str, P], R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
request_id = "req-1234"
return fn(request_id, *args, **kwargs)
return wrapper
@with_request_id
def handler(request_id: str, payload: dict) -> int:
print(request_id, payload)
return 200
print(handler({"x": 1})) # request_id is supplied by the decorator
Output:
req-1234 {'x': 1}
200
Concatenate[str, P] says "the wrapped function takes a string followed by P". Type checkers verify the call site sees only P after wrapping.
Recipe 6 — assert_never for exhaustive match.
from typing_extensions import assert_never, Literal
Color = Literal["red", "green", "blue"]
def to_hex(c: Color) -> str:
if c == "red": return "#ff0000"
if c == "green": return "#00ff00"
if c == "blue": return "#0000ff"
assert_never(c) # mypy / pyright errors if a case is missing
Output: runtime never reaches assert_never for valid Color values; type checkers verify the function is exhaustive at edit time.
Performance tuning
typing-extensions itself has no runtime perf — most constructs are no-ops at runtime. The overhead is in:
TypedDictvalidation libraries (pydantic, msgspec) that consume the annotations. Tune those, nottyping-extensions.Annotatedmetadata extraction —get_type_hints(..., include_extras=True)is the canonical way.runtime_checkableProtocols — usingisinstance(x, MyProtocol)is slower than nominalisinstance. Reserve for true duck-typing tests.
Version migration guide
The migration story is "remove typing_extensions imports as you raise your Python floor":
- 3.8 floor —
Literal,TypedDict,Final,Protocol,runtime_checkablemove to stdlib. - 3.9 floor —
Annotatedmoves to stdlib.list[int]/dict[str, T]generics work directly withoutfrom __future__ import annotations. - 3.10 floor —
ParamSpec,Concatenate,TypeAlias,TypeGuard,is_typeddictmove to stdlib. - 3.11 floor —
Self,Never,Required,NotRequired,Unpack,assert_never,assert_typemove to stdlib. - 3.12 floor —
TypeAliasType,overridemove to stdlib. - 3.13 floor —
ReadOnlyfor TypedDict,TypeIsmove to stdlib.
# Pre-3.11
from typing_extensions import Self
# 3.11+: stdlib equivalent
from typing import Self
Output: functionally identical; the stdlib path avoids the third-party dependency. Linting rules like ruff's UP037 can flag these.
Security considerations
typing-extensionshas no runtime side effects. No filesystem, no network. Practically the safest possible dependency.- Don't use
runtime_checkableProtocols for security-sensitive checks. They check method existence only, not signature compatibility. Annotatedmetadata is untyped at runtime. Anything that consumes metadata (pydantic, etc.) is responsible for validating it; don't trust attacker-controlled metadata.
Testing & CI
Type-checking is the entire point. CI integration is via mypy / pyright:
# pip install mypy
mypy --strict src/
Output:
Success: no issues found in N source files.
Failures show specific lines where the type checker disagreed with the annotation — usually a missing NotRequired, a mis-named Self, or a forgotten assert_never.
Ecosystem integrations
mypy,pyright,pyre— all recognizetyping_extensionsconstructs and treat them identically to stdlib equivalents.pydantic—Annotatedis the primary mechanism for field constraints (e.g.Annotated[int, Field(ge=0)]).fastapi— usesAnnotatedforDepends,Query,Pathparameter metadata.msgspec— sameAnnotatedpattern.sqlalchemy 2.x— usesAnnotatedfor mapped column types.starlette,litestar— depend on it viapydantic.
Compatibility matrix
| Python | typing-extensions | Notes |
|---|---|---|
| 3.6 | 4.0.x (frozen) | Final supported line for 3.6. |
| 3.7 | 4.7.x (frozen) | Final line for 3.7. |
| 3.8 | 4.x | Lowest current floor. |
| 3.9 | 4.x | Stable. |
| 3.10 | 4.x | Stable. |
| 3.11 | 4.x | Stable. |
| 3.12 | 4.x | Stable. |
| 3.13 | 4.x | Stable; new constructs land here first. |
Production deployment
- Pin a minimum version in libraries (
typing-extensions>=4.6) so consumers get the constructs you actually use. Tight pins cause solver conflicts. - Keep the import idiomatic.
from typing_extensions import X— neverimport typing_extensions as te; te.X. - Track stdlib parity. When you raise your Python floor, run
ruff --select UP037(or equivalent) to migrate imports back to stdlib. - Don't conditional-import.
try: from typing import X except: from typing_extensions import Xis brittle and unnecessary — just import fromtyping_extensionsalways; it re-exports the stdlib version on newer Pythons.
When NOT to use this
- Your minimum Python is 3.13+ — most useful constructs are stdlib already; the package mostly forwards.
- You don't use type hints — the entire library is no-ops without a type checker.
- You're shipping a single-file script for a known Python version — direct
from typing import ...is simpler.
Troubleshooting common errors
| Error / Symptom | Likely cause | Fix |
|---|---|---|
ImportError: cannot import name 'Self' from 'typing' | Running on Python <3.11 | Import from typing_extensions instead. |
Type checker says Self is unknown | Type checker too old | Upgrade mypy/pyright. |
TypedDict accepts arbitrary keys at runtime | TypedDict is checker-only | Use pydantic or msgspec for runtime validation. |
NotRequired ignored at runtime | NotRequired is checker-only | Same — use a validation library. |
pip install resolves an old typing-extensions | Old transitive pin | Inspect with pip show typing-extensions; bump library that pins old version. |
See also
- Python: typing — stdlib typing fundamentals
- Python: mypy — static type checker
- Official typing-extensions docs
- PEP 655 — Required / NotRequired