about things notes.zzstoatzz.io
notes
4.4 kB
Markdown
at main

typing #

notes on type hints as actually used in projects like pdsx (an atproto record CLI + MCP server) and fastmcp (an mcp server framework).

unions #

use | for unions, not Optional:

RecordValue = str | int | float | bool | None | dict[str, Any] | list[Any]

from pdsx/_internal/types.py (pdsx is an atproto record CLI + MCP server)

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

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:

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 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:

@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:

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:

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

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 #