Runtime vs Static
Python typing lives in two different worlds at once: the static world of type checkers and the runtime world of frameworks that inspect annotations directly. If you blur those worlds together, `Annotated`, deferred annotations, FastAPI, and Pydantic quickly become confusing.
Quick takeaway: type checkers read annotations for static meaning, while runtime tools read annotations as objects or expressions they can inspect. `Annotated` and `annotationlib` are especially important at this boundary.
Separate the Two Views
A Small Example
from typing import Annotated
import annotationlib
def endpoint(
user_id: Annotated[int, "path parameter"],
) -> Annotated[str, "response body"]:
return str(user_id)
print(
annotationlib.get_annotations(
endpoint,
format=annotationlib.Format.STRING,
)
)
print(endpoint.__annotations__)A type checker mainly cares about `int` and `str`. Runtime tools can also inspect metadata inside `Annotated` and can work with deferred annotation forms through `annotationlib`.
What FastAPI and Pydantic Consume
- FastAPI reads parameter annotations and
Annotatedmetadata to build request parsing and OpenAPI descriptions. - Pydantic reads annotations to build core schema for validation and serialization.
- So annotations are no longer just static comments for tooling.
Key Questions
- Why do type checkers know things the runtime does not?
- Which parts of an annotation are types and which parts are metadata?
Checklist
Separate static and runtime meaning
Type checkers infer without executing code; frameworks read annotation objects at runtime.
Use `Annotated` intentionally
It is a strong tool for carrying both type meaning and runtime metadata, but the two roles should stay conceptually separate.
Understand annotation reading cost
Deferred annotations and `annotationlib` exist partly to reduce forward-reference and import-cycle pain.
Know how frameworks consume annotations
FastAPI, Pydantic, and ORMs can treat annotations as runtime metadata, not just type hints.