typing#
notes on type hints as actually used in projects like pdsx and fastmcp.
unions#
use | for unions, not Optional:
RecordValue = str | int | float | bool | None | dict[str, Any] | list[Any]
TypedDict#
for structured dictionaries where you know the shape:
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
Annotated#
attach metadata to types. useful for documentation and schema generation:
from typing import Annotated
from pydantic import Field
FilterParam = Annotated[
str | None,
Field(
description=(
"jmespath expression to filter/project the result. "
"examples: '[*].{uri: uri, text: value.text}' (select fields), "
"'[?value.text != null]' (filter items), "
"'[*].uri' (extract values)"
),
),
]
the metadata travels with the type. MCP tools use this for parameter descriptions.
Protocol#
define what methods something needs, not what class it is:
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
generics#
TypeVar for generic functions and classes:
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def filterable(
fn: Callable[P, R] | Callable[P, Awaitable[R]],
) -> Callable[P, Any] | Callable[P, Awaitable[Any]]:
...
ParamSpec captures the full signature (args and kwargs) for decorator typing.
ParamSpec#
for decorators that preserve function signatures:
@wraps(fn)
async def async_wrapper(
*args: P.args, _filter: str | None = None, **kwargs: P.kwargs
) -> Any:
result = await fn(*args, **kwargs)
return apply_filter(result, _filter)
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 (plus any new parameters).
overload#
when return type depends on input type:
from typing import overload
@overload
def filterable(
fn: Callable[P, Awaitable[R]],
) -> Callable[P, Awaitable[Any]]: ...
@overload
def filterable(
fn: Callable[P, R],
) -> Callable[P, Any]: ...
def filterable(
fn: Callable[P, R] | Callable[P, Awaitable[R]],
) -> Callable[P, Any] | Callable[P, Awaitable[Any]]:
...
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:
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.
import organization#
"""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: