# typing notes on type hints as actually used in projects like [pdsx](https://github.com/zzstoatzz/pdsx) (an atproto record CLI + MCP server) and [fastmcp](https://github.com/jlowin/fastmcp) (an mcp server framework). ## unions use `|` for unions, not `Optional`: ```python RecordValue = str | int | float | bool | None | dict[str, Any] | list[Any] ``` from [pdsx/_internal/types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/types.py) (pdsx is an atproto record CLI + MCP server) ## TypedDict for structured dictionaries where you know the shape: ```python from typing import TypedDict class RecordResponse(TypedDict): """a record returned from list or get operations.""" uri: str cid: str | None value: dict class CredentialsContext(TypedDict): """credentials extracted from context or headers.""" handle: str | None password: str | None pds_url: str | None repo: str | None ``` better than `dict[str, Any]` because the structure is documented and checked. from [pdsx/mcp/_types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/_types.py) ## Annotated attach metadata to types. useful for documentation and schema generation: ```python from typing import Annotated from pydantic import Field Limit = Annotated[ int, Field(description="maximum number of records to return", gt=0, le=100), ] ``` the metadata travels with the type. MCP tools use this for parameter descriptions — fastmcp folds the Field description into the generated schema. ## Protocol define what methods something needs, not what class it is: ```python from typing import Protocol class ContextSamplingFallbackProtocol(Protocol): async def __call__( self, messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None, ) -> ContentBlock: ... ``` any callable matching this signature satisfies the protocol. no inheritance required. from [fastmcp/utilities/types.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py) ## generics TypeVar for generic functions and classes: ```python from typing import ParamSpec, TypeVar P = ParamSpec("P") R = TypeVar("R") def traced( fn: Callable[P, R] | Callable[P, Awaitable[R]], ) -> Callable[P, R] | Callable[P, Awaitable[R]]: ... ``` `ParamSpec` captures the full signature (args and kwargs) for decorator typing. ## ParamSpec for decorators that preserve function signatures: ```python @wraps(fn) async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: log.debug("calling %s", fn.__name__) return await fn(*args, **kwargs) ``` `P.args` and `P.kwargs` carry the original function's parameter types into the wrapper. type checkers see the wrapper with the same signature as the wrapped function. ## overload when return type depends on input type: ```python from typing import overload @overload def traced( fn: Callable[P, Awaitable[R]], ) -> Callable[P, Awaitable[R]]: ... @overload def traced( fn: Callable[P, R], ) -> Callable[P, R]: ... def traced( fn: Callable[P, R] | Callable[P, Awaitable[R]], ) -> Callable[P, R] | Callable[P, Awaitable[R]]: ... ``` type checkers know async functions get async wrappers, sync functions get sync wrappers. ## TYPE_CHECKING avoid runtime import costs for types only needed for hints: ```python from typing import TYPE_CHECKING if TYPE_CHECKING: from docket import Docket from docket.execution import Execution from fastmcp.tools.tool_transform import ArgTransform, TransformedTool ``` the import doesn't happen at runtime, only when type checkers analyze the code. with `from __future__ import annotations`, you don't need string quotes. from [fastmcp/tools/tool.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py) ## import organization ```python """module docstring.""" from __future__ import annotations import stdlib_module from typing import TYPE_CHECKING from third_party import thing from local_package import helper if TYPE_CHECKING: from expensive import Type ``` ## sources - [pdsx/src/pdsx/_internal/types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/types.py) - [fastmcp/src/fastmcp/tools/tool.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py)