1# typing 2 3notes on type hints as actually used in projects like pdsx and fastmcp. 4 5## unions 6 7use `|` for unions, not `Optional`: 8 9```python 10RecordValue = str | int | float | bool | None | dict[str, Any] | list[Any] 11``` 12 13from [pdsx/_internal/types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/types.py) 14 15## TypedDict 16 17for structured dictionaries where you know the shape: 18 19```python 20from typing import TypedDict 21 22class RecordResponse(TypedDict): 23 """a record returned from list or get operations.""" 24 uri: str 25 cid: str | None 26 value: dict 27 28class CredentialsContext(TypedDict): 29 """credentials extracted from context or headers.""" 30 handle: str | None 31 password: str | None 32 pds_url: str | None 33 repo: str | None 34``` 35 36better than `dict[str, Any]` because the structure is documented and checked. 37 38from [pdsx/mcp/_types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/_types.py) 39 40## Annotated 41 42attach metadata to types. useful for documentation and schema generation: 43 44```python 45from typing import Annotated 46from pydantic import Field 47 48FilterParam = Annotated[ 49 str | None, 50 Field( 51 description=( 52 "jmespath expression to filter/project the result. " 53 "examples: '[*].{uri: uri, text: value.text}' (select fields), " 54 "'[?value.text != null]' (filter items), " 55 "'[*].uri' (extract values)" 56 ), 57 ), 58] 59``` 60 61the metadata travels with the type. MCP tools use this for parameter descriptions. 62 63from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 64 65## Protocol 66 67define what methods something needs, not what class it is: 68 69```python 70from typing import Protocol 71 72class ContextSamplingFallbackProtocol(Protocol): 73 async def __call__( 74 self, 75 messages: str | list[str | SamplingMessage], 76 system_prompt: str | None = None, 77 temperature: float | None = None, 78 max_tokens: int | None = None, 79 ) -> ContentBlock: ... 80``` 81 82any callable matching this signature satisfies the protocol. no inheritance required. 83 84from [fastmcp/utilities/types.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py) 85 86## generics 87 88TypeVar for generic functions and classes: 89 90```python 91from typing import ParamSpec, TypeVar 92 93P = ParamSpec("P") 94R = TypeVar("R") 95 96def filterable( 97 fn: Callable[P, R] | Callable[P, Awaitable[R]], 98) -> Callable[P, Any] | Callable[P, Awaitable[Any]]: 99 ... 100``` 101 102`ParamSpec` captures the full signature (args and kwargs) for decorator typing. 103 104from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 105 106## ParamSpec 107 108for decorators that preserve function signatures: 109 110```python 111@wraps(fn) 112async def async_wrapper( 113 *args: P.args, _filter: str | None = None, **kwargs: P.kwargs 114) -> Any: 115 result = await fn(*args, **kwargs) 116 return apply_filter(result, _filter) 117``` 118 119`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). 120 121from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 122 123## overload 124 125when return type depends on input type: 126 127```python 128from typing import overload 129 130@overload 131def filterable( 132 fn: Callable[P, Awaitable[R]], 133) -> Callable[P, Awaitable[Any]]: ... 134 135@overload 136def filterable( 137 fn: Callable[P, R], 138) -> Callable[P, Any]: ... 139 140def filterable( 141 fn: Callable[P, R] | Callable[P, Awaitable[R]], 142) -> Callable[P, Any] | Callable[P, Awaitable[Any]]: 143 ... 144``` 145 146type checkers know async functions get async wrappers, sync functions get sync wrappers. 147 148from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 149 150## TYPE_CHECKING 151 152avoid runtime import costs for types only needed for hints: 153 154```python 155from typing import TYPE_CHECKING 156 157if TYPE_CHECKING: 158 from docket import Docket 159 from docket.execution import Execution 160 from fastmcp.tools.tool_transform import ArgTransform, TransformedTool 161``` 162 163the import doesn't happen at runtime, only when type checkers analyze the code. with `from __future__ import annotations`, you don't need string quotes. 164 165from [fastmcp/tools/tool.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py) 166 167## import organization 168 169```python 170"""module docstring.""" 171 172from __future__ import annotations 173 174import stdlib_module 175from typing import TYPE_CHECKING 176 177from third_party import thing 178 179from local_package import helper 180 181if TYPE_CHECKING: 182 from expensive import Type 183``` 184 185sources: 186- [pdsx/src/pdsx/_internal/types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/types.py) 187- [fastmcp/src/fastmcp/tools/tool.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py)