typing.Annotated
typing.Annotated
https://docs.python.org/3/library/typing.html
Special typing form to add context-specific metadata to an annotation.
Add metadata x
to a given type T
by using the annotation Annotated[T, x]
. Metadata added using Annotated
can be used by static analysis tools or at runtime. At runtime, the metadata is stored in a __metadata__
attribute.
If a library or tool encounters an annotation Annotated[T, x]
and has no special logic for the metadata, it should ignore the metadata and simply treat the annotation as T
. As such, Annotated
can be useful for code that wants to use annotations for purposes outside Python’s static typing system.
Using Annotated[T, x]
as an annotation still allows for static typechecking of T
, as type checkers will simply ignore the metadata x
. In this way, Annotated
differs from the @no_type_check
decorator, which can also be used for adding annotations outside the scope of the typing system, but completely disables typechecking for a function or class.
The responsibility of how to interpret the metadata lies with the tool or library encountering an Annotated
annotation. A tool or library encountering an Annotated
type can scan through the metadata elements to determine if they are of interest (e.g., using isinstance()
).
- Annotated[<type>, <metadata>]
Here is an example of how you might use Annotated
to add metadata to type annotations if you were doing range analysis:
@dataclass
class ValueRange:
lo: int
hi: int
T1 = Annotated[int, ValueRange(-10, 5)]
T2 = Annotated[T1, ValueRange(-20, 3)]
The first argument to Annotated
must be a valid type. Multiple metadata elements can be supplied as Annotated
supports variadic arguments. The order of the metadata elements is preserved and matters for equality checks:
@dataclass
class ctype:
kind: str
a1 = Annotated[int, ValueRange(3, 10), ctype("char")]
a2 = Annotated[int, ctype("char"), ValueRange(3, 10)]
assert a1 != a2 # Order matters
It is up to the tool consuming the annotations to decide whether the client is allowed to add multiple metadata elements to one annotation and how to merge those annotations.
Nested Annotated
types are flattened. The order of the metadata elements starts with the innermost annotation:
assert Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
int, ValueRange(3, 10), ctype("char")
]
Duplicated metadata elements are not removed:
assert Annotated[int, ValueRange(3, 10)] != Annotated[
int, ValueRange(3, 10), ValueRange(3, 10)
]
Annotated
can be used with nested and generic aliases:
@dataclass class MaxLen: value: int type Vec[T] = Annotated[list[tuple[T, T]], MaxLen(10)] # When used in a type annotation, a type checker will treat "V" the same as # ``Annotated[list[tuple[int, int]], MaxLen(10)]``: type V = Vec[int]
Annotated
cannot be used with an unpacked TypeVarTuple
:
type Variadic[*Ts] = Annotated[*Ts, Ann1] = Annotated[T1, T2, T3, ..., Ann1] # NOT valid
where T1
, T2
, … are TypeVars
. This is invalid as only one type should be passed to Annotated.
By default, get_type_hints()
strips the metadata from annotations. Pass include_extras=True
to have the metadata preserved:
from typing import Annotated, get_type_hints
def func(x: Annotated[int, "metadata"]) -> None: pass
get_type_hints(func)
{'x': <class 'int'>, 'return': <class 'NoneType'>}
get_type_hints(func, include_extras=True) {'x': typing.Annotated[int, 'metadata'], 'return': <class 'NoneType'>}
At runtime, the metadata associated with an Annotated
type can be retrieved via the __metadata__
attribute:
from typing import Annotated
X = Annotated[int, "very", "important", "metadata"]
X
typing.Annotated[int, 'very', 'important', 'metadata']
X.__metadata__ ('very', 'important', 'metadata')
If you want to retrieve the original type wrapped by Annotated
, use the __origin__
attribute:
from typing import Annotated, get_origin
Password = Annotated[str, "secret"]
Password.__origin__ <class 'str'>
Note that using get_origin()
will return Annotated
itself:
get_origin(Password) typing.Annotated
See also
- PEP 593 - Flexible function and variable annotations
-
The PEP introducing
Annotated
to the standard library.
Added in version 3.9.
https://github.com/fanqingsong/fastapi-mcp-langgraph-template
from typing import AsyncGenerator import psycopg.errors from fastapi import APIRouter from langchain_core.messages import HumanMessage from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from sse_starlette.sse import EventSourceResponse from starlette.responses import Response from api.core.agent.orchestration import get_config, get_graph from api.core.dependencies import LLMDep, setup_graph from api.core.logs import print, uvicorn router = APIRouter(tags=["chat"]) @router.get("/chat/completions") async def completions(query: str, llm: LLMDep) -> Response: """ Stream model completions as Server-Sent Events (SSE). This endpoint sends the model's responses in real-time as they are generated, allowing for a continuous stream of data to the client. """ return EventSourceResponse(stream_completions(query, llm)) @router.get("/chat/agent") async def agent(query: str, llm: LLMDep) -> Response: """Stream LangGraph completions as Server-Sent Events (SSE). This endpoint streams LangGraph-generated events in real-time, allowing the client to receive responses as they are processed, useful for agent-based workflows. """ return EventSourceResponse(stream_graph(query, llm)) async def stream_completions( query: str, llm: LLMDep ) -> AsyncGenerator[dict[str, str], None]: async for chunk in llm.astream_events(query): yield dict(data=chunk) async def checkpointer_setup(pool): checkpointer = AsyncPostgresSaver(pool) try: await checkpointer.setup() except ( psycopg.errors.DuplicateColumn, psycopg.errors.ActiveSqlTransaction, ): uvicorn.warning("Skipping checkpointer setup — already configured.") return checkpointer async def stream_graph( query: str, llm: LLMDep, ) -> AsyncGenerator[dict[str, str], None]: async with setup_graph() as resource: graph = get_graph( llm, tools=resource.tools, checkpointer=resource.checkpointer, ) config = get_config() events = dict(messages=[HumanMessage(content=query)]) async for event in graph.astream_events(events, config, version="v2"): if event.get("event").endswith("end"): print(event) yield dict(data=event)
from contextlib import asynccontextmanager from typing import Annotated, AsyncGenerator from fastapi import Depends from langchain_mcp_adapters.tools import load_mcp_tools from langchain_openai import ChatOpenAI from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from api.core.agent.persistence import checkpointer_context from api.core.config import settings from api.core.mcps import mcp_sse_client from api.core.models import Resource def get_llm() -> ChatOpenAI: return ChatOpenAI( streaming=True, model=settings.model, temperature=0, api_key=settings.openai_api_key, stream_usage=True, ) LLMDep = Annotated[ChatOpenAI, Depends(get_llm)] engine: AsyncEngine = create_async_engine(settings.orm_conn_str) def get_engine() -> AsyncEngine: return engine EngineDep = Annotated[AsyncEngine, Depends(get_engine)] @asynccontextmanager async def setup_graph() -> AsyncGenerator[Resource]: async with checkpointer_context( settings.checkpoint_conn_str ) as checkpointer: async with mcp_sse_client() as session: tools = await load_mcp_tools(session) yield Resource( checkpointer=checkpointer, tools=tools, session=session, )