reorganize python notes, audit examples against real code

- restructure into language/ and ecosystem/ subdirectories
- trim README commentary
- verify all code examples against pdsx, fastmcp, docket
- add source links to examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+11 -15
languages/python/README.md
··· 1 1 # python 2 2 3 - notes on writing python. 3 + assumes 3.12+. 4 4 5 - python is dynamically typed but increasingly written with static type hints. it has first-class async support but doesn't force it on you. it has a global interpreter lock but that matters less than people think for I/O-bound work. 5 + ## language 6 6 7 - these notes assume python 3.12 or later. 3.12 introduced native generic syntax and the `type` statement - the typing story finally feels complete. 8 - 9 - ## the language 10 - 11 - - [typing](./typing.md) - type hints, generics, Protocol 12 - - [async](./async.md) - async with, ContextVar, concurrency 13 - - [patterns](./patterns.md) - class design, decorators, error handling 7 + - [typing](./language/typing.md) 8 + - [async](./language/async.md) 9 + - [patterns](./language/patterns.md) 14 10 15 - ## the ecosystem 11 + ## ecosystem 16 12 17 - - [uv](./uv.md) - package management that doesn't hurt 18 - - [project setup](./project-setup.md) - src/ layout, pyproject.toml, justfile 19 - - [tooling](./tooling.md) - ruff, ty, pre-commit 20 - - [pydantic](./pydantic.md) - validation at boundaries 21 - - [mcp](./mcp.md) - building tools for LLMs 13 + - [uv](./ecosystem/uv.md) 14 + - [project setup](./ecosystem/project-setup.md) 15 + - [tooling](./ecosystem/tooling.md) 16 + - [pydantic](./ecosystem/pydantic.md) 17 + - [mcp](./ecosystem/mcp.md) 22 18 23 19 ## sources 24 20
-60
languages/python/async.md
··· 1 - # async 2 - 3 - python's `async`/`await` syntax is straightforward. the interesting part is how you structure code around it. 4 - 5 - ## async with 6 - 7 - the core insight from async python codebases: `async with` is how you manage resources. not try/finally, not callbacks - the context manager protocol. 8 - 9 - when you open a connection, start a session, or acquire any resource that needs cleanup, you wrap it in an async context manager: 10 - 11 - ```python 12 - @asynccontextmanager 13 - async def get_client() -> AsyncIterator[Client]: 14 - client = Client() 15 - await client.connect() 16 - try: 17 - yield client 18 - finally: 19 - await client.close() 20 - ``` 21 - 22 - the caller writes `async with get_client() as c:` and cleanup happens automatically, even if exceptions occur. this pattern appears constantly - database connections, HTTP sessions, file handles, locks. 23 - 24 - the alternative - manual try/finally blocks scattered through the code, or worse, forgetting cleanup entirely - is why this pattern dominates. you encode the lifecycle once in the context manager, and every use site gets it right by default. 25 - 26 - ## ContextVar 27 - 28 - python added `contextvars` to solve a specific problem: how do you have request-scoped state in async code without passing it through every function? 29 - 30 - in sync code, you might use thread-locals. but async tasks can interleave on the same thread, so thread-locals don't work. `ContextVar` gives each task its own copy: 31 - 32 - ```python 33 - from contextvars import ContextVar 34 - 35 - _current_request: ContextVar[Request | None] = ContextVar("request", default=None) 36 - ``` 37 - 38 - set it at the start of handling a request, and any code called from that task can access it. this is how frameworks like fastapi and fastmcp pass request context without threading it through every function signature. 39 - 40 - the pattern: set at the boundary (request handler, task entry), read anywhere inside. reset when you're done. 41 - 42 - ## concurrency control 43 - 44 - `asyncio.gather()` runs tasks concurrently, but sometimes you need to limit how many run at once - rate limits, connection pools, memory constraints. 45 - 46 - `asyncio.Semaphore` is the primitive for this. acquire before work, release after. the `async with` syntax makes it clean: 47 - 48 - ```python 49 - semaphore = asyncio.Semaphore(10) 50 - 51 - async def limited_work(item): 52 - async with semaphore: 53 - return await do_work(item) 54 - ``` 55 - 56 - at most 10 `do_work` calls run concurrently. the rest wait. 57 - 58 - sources: 59 - - [pdsx/src/pdsx/mcp/client.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/client.py) 60 - - [docket/src/docket/worker.py](https://github.com/chrisguidry/docket/blob/main/src/docket/worker.py)
+79
languages/python/ecosystem/pydantic.md
··· 1 + # pydantic 2 + 3 + pydantic is a library, not the language. but it's become foundational enough that it's worth understanding. 4 + 5 + ## the core idea 6 + 7 + python's type hints don't do anything at runtime by default. `def foo(x: int)` accepts strings, floats, whatever - the annotation is just documentation. 8 + 9 + pydantic makes them real. define a model with type hints, and pydantic validates and coerces data to match: 10 + 11 + ```python 12 + from pydantic import BaseModel 13 + 14 + class User(BaseModel): 15 + name: str 16 + age: int 17 + 18 + user = User(name="alice", age="25") # age coerced to int 19 + user = User(name="alice", age="not a number") # raises ValidationError 20 + ``` 21 + 22 + this is why pydantic shows up everywhere in python - it bridges the gap between python's dynamic runtime and the desire for validated, typed data. 23 + 24 + ## settings from environment 25 + 26 + the most common use: replacing `os.getenv()` calls with validated configuration. 27 + 28 + ```python 29 + from pydantic import Field 30 + from pydantic_settings import BaseSettings, SettingsConfigDict 31 + 32 + class Settings(BaseSettings): 33 + """settings for atproto cli.""" 34 + 35 + model_config = SettingsConfigDict( 36 + env_file=str(Path.cwd() / ".env"), 37 + env_file_encoding="utf-8", 38 + extra="ignore", 39 + case_sensitive=False, 40 + ) 41 + 42 + atproto_pds_url: str = Field( 43 + default="https://bsky.social", 44 + description="PDS URL", 45 + ) 46 + atproto_handle: str = Field(default="", description="Your atproto handle") 47 + atproto_password: str = Field(default="", description="Your atproto app password") 48 + 49 + settings = Settings() 50 + ``` 51 + 52 + `model_config` controls where settings come from (environment, .env files) and how to handle unknowns. required fields without defaults fail at import time - not later when you try to use them. 53 + 54 + from [pdsx/_internal/config.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/config.py) 55 + 56 + ## when to use what 57 + 58 + pydantic models are heavier than they look - they do a lot of work on instantiation. for internal data you control, python's `dataclasses` are simpler: 59 + 60 + ```python 61 + from dataclasses import dataclass 62 + 63 + @dataclass 64 + class BatchResult: 65 + """result of a batch operation.""" 66 + successful: list[str] 67 + failed: list[tuple[str, Exception]] 68 + 69 + @property 70 + def total(self) -> int: 71 + return len(self.successful) + len(self.failed) 72 + ``` 73 + 74 + no validation, no coercion, just a class with fields. use pydantic at boundaries (API input, config files, external data) where you need validation. use dataclasses for internal data structures. 75 + 76 + from [pdsx/_internal/batch.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/batch.py) 77 + 78 + sources: 79 + - [how to use pydantic-settings](https://blog.zzstoatzz.io/how-to-use-pydantic-settings/)
+79
languages/python/language/async.md
··· 1 + # async 2 + 3 + python's `async`/`await` syntax is straightforward. the interesting part is how you structure code around it. 4 + 5 + ## async with 6 + 7 + the core insight from async python codebases: `async with` is how you manage resources. not try/finally, not callbacks - the context manager protocol. 8 + 9 + when you open a connection, start a session, or acquire any resource that needs cleanup, you wrap it in an async context manager: 10 + 11 + ```python 12 + @asynccontextmanager 13 + async def get_atproto_client( 14 + require_auth: bool = False, 15 + operation: str = "this operation", 16 + target_repo: str | None = None, 17 + ) -> AsyncIterator[AsyncClient]: 18 + """get an atproto client using credentials from context or environment.""" 19 + client = AsyncClient(pds_url) 20 + if require_auth and handle and password: 21 + await client.login(handle, password) 22 + try: 23 + yield client 24 + finally: 25 + pass # AsyncClient doesn't need explicit cleanup 26 + ``` 27 + 28 + the caller writes `async with get_atproto_client() as client:` and cleanup happens automatically. this pattern appears constantly - database connections, HTTP sessions, file handles, locks. 29 + 30 + from [pdsx/mcp/client.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/client.py) 31 + 32 + the alternative - manual try/finally blocks scattered through the code, or worse, forgetting cleanup entirely - is why this pattern dominates. you encode the lifecycle once in the context manager, and every use site gets it right by default. 33 + 34 + ## ContextVar 35 + 36 + python added `contextvars` to solve a specific problem: how do you have request-scoped state in async code without passing it through every function? 37 + 38 + in sync code, you might use thread-locals. but async tasks can interleave on the same thread, so thread-locals don't work. `ContextVar` gives each task its own copy: 39 + 40 + ```python 41 + from contextvars import ContextVar 42 + 43 + _current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None) 44 + _current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None) 45 + _current_server: ContextVar[weakref.ref[FastMCP] | None] = ContextVar("server", default=None) 46 + ``` 47 + 48 + set it at the start of handling a request, and any code called from that task can access it. this is how frameworks like fastapi and fastmcp pass request context without threading it through every function signature. 49 + 50 + the pattern: set at the boundary (request handler, task entry), read anywhere inside. reset when you're done. 51 + 52 + from [fastmcp/server/dependencies.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py) 53 + 54 + ## concurrency control 55 + 56 + `asyncio.gather()` runs tasks concurrently, but sometimes you need to limit how many run at once - rate limits, connection pools, memory constraints. 57 + 58 + `asyncio.Semaphore` is the primitive for this. acquire before work, release after. the `async with` syntax makes it clean: 59 + 60 + ```python 61 + semaphore = asyncio.Semaphore(concurrency) 62 + 63 + async def delete_one(uri: str) -> None: 64 + """delete a single record with concurrency control.""" 65 + async with semaphore: 66 + try: 67 + await delete_record(client, uri) 68 + successful.append(uri) 69 + except Exception as e: 70 + failed.append((uri, e)) 71 + if fail_fast: 72 + raise 73 + 74 + await asyncio.gather(*[delete_one(uri) for uri in uris]) 75 + ``` 76 + 77 + at most `concurrency` delete operations run at once. the rest wait. 78 + 79 + from [pdsx/_internal/batch.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/batch.py)
+187
languages/python/language/typing.md
··· 1 + # typing 2 + 3 + notes on type hints as actually used in projects like pdsx and fastmcp. 4 + 5 + ## unions 6 + 7 + use `|` for unions, not `Optional`: 8 + 9 + ```python 10 + RecordValue = str | int | float | bool | None | dict[str, Any] | list[Any] 11 + ``` 12 + 13 + from [pdsx/_internal/types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/types.py) 14 + 15 + ## TypedDict 16 + 17 + for structured dictionaries where you know the shape: 18 + 19 + ```python 20 + from typing import TypedDict 21 + 22 + class RecordResponse(TypedDict): 23 + """a record returned from list or get operations.""" 24 + uri: str 25 + cid: str | None 26 + value: dict 27 + 28 + class 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 + 36 + better than `dict[str, Any]` because the structure is documented and checked. 37 + 38 + from [pdsx/mcp/_types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/_types.py) 39 + 40 + ## Annotated 41 + 42 + attach metadata to types. useful for documentation and schema generation: 43 + 44 + ```python 45 + from typing import Annotated 46 + from pydantic import Field 47 + 48 + FilterParam = 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 + 61 + the metadata travels with the type. MCP tools use this for parameter descriptions. 62 + 63 + from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 64 + 65 + ## Protocol 66 + 67 + define what methods something needs, not what class it is: 68 + 69 + ```python 70 + from typing import Protocol 71 + 72 + class 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 + 82 + any callable matching this signature satisfies the protocol. no inheritance required. 83 + 84 + from [fastmcp/utilities/types.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py) 85 + 86 + ## generics 87 + 88 + TypeVar for generic functions and classes: 89 + 90 + ```python 91 + from typing import ParamSpec, TypeVar 92 + 93 + P = ParamSpec("P") 94 + R = TypeVar("R") 95 + 96 + def 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 + 104 + from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 105 + 106 + ## ParamSpec 107 + 108 + for decorators that preserve function signatures: 109 + 110 + ```python 111 + @wraps(fn) 112 + async 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 + 121 + from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 122 + 123 + ## overload 124 + 125 + when return type depends on input type: 126 + 127 + ```python 128 + from typing import overload 129 + 130 + @overload 131 + def filterable( 132 + fn: Callable[P, Awaitable[R]], 133 + ) -> Callable[P, Awaitable[Any]]: ... 134 + 135 + @overload 136 + def filterable( 137 + fn: Callable[P, R], 138 + ) -> Callable[P, Any]: ... 139 + 140 + def filterable( 141 + fn: Callable[P, R] | Callable[P, Awaitable[R]], 142 + ) -> Callable[P, Any] | Callable[P, Awaitable[Any]]: 143 + ... 144 + ``` 145 + 146 + type checkers know async functions get async wrappers, sync functions get sync wrappers. 147 + 148 + from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 149 + 150 + ## TYPE_CHECKING 151 + 152 + avoid runtime import costs for types only needed for hints: 153 + 154 + ```python 155 + from typing import TYPE_CHECKING 156 + 157 + if TYPE_CHECKING: 158 + from docket import Docket 159 + from docket.execution import Execution 160 + from fastmcp.tools.tool_transform import ArgTransform, TransformedTool 161 + ``` 162 + 163 + 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. 164 + 165 + from [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 + 172 + from __future__ import annotations 173 + 174 + import stdlib_module 175 + from typing import TYPE_CHECKING 176 + 177 + from third_party import thing 178 + 179 + from local_package import helper 180 + 181 + if TYPE_CHECKING: 182 + from expensive import Type 183 + ``` 184 + 185 + sources: 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)
languages/python/mcp.md languages/python/ecosystem/mcp.md
+25 -4
languages/python/patterns.md languages/python/language/patterns.md
··· 37 37 38 38 lighter than pydantic when you control the data source. 39 39 40 + from [pdsx/_internal/batch.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/batch.py) 41 + 40 42 ## base classes with parallel implementations 41 43 42 44 when you need both sync and async: ··· 87 89 ```python 88 90 @dataclass 89 91 class URIParts: 92 + """parsed components of an AT-URI.""" 90 93 repo: str 91 94 collection: str 92 95 rkey: str 93 96 94 97 @classmethod 95 - def from_uri(cls, uri: str) -> "URIParts": 96 - # at://did:plc:xxx/app.bsky.feed.post/abc 97 - parts = uri.replace("at://", "").split("/") 98 - return cls(repo=parts[0], collection=parts[1], rkey=parts[2]) 98 + def from_uri(cls, uri: str, client_did: str | None = None) -> URIParts: 99 + """parse an AT-URI into its components.""" 100 + uri_without_prefix = uri.replace("at://", "") 101 + parts = uri_without_prefix.split("/") 102 + 103 + # shorthand format: collection/rkey 104 + if len(parts) == 2: 105 + if not client_did: 106 + raise ValueError("shorthand URI requires authentication") 107 + return cls(repo=client_did, collection=parts[0], rkey=parts[1]) 108 + 109 + # full format: did/collection/rkey 110 + if len(parts) == 3: 111 + return cls(repo=parts[0], collection=parts[1], rkey=parts[2]) 112 + 113 + raise ValueError(f"invalid URI format: {uri}") 99 114 ``` 115 + 116 + from [pdsx/_internal/resolution.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/resolution.py) 100 117 101 118 ## keyword-only arguments 102 119 ··· 131 148 ``` 132 149 133 150 the message tells you what to do, not just what went wrong. 151 + 152 + from [pdsx/mcp/client.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/client.py) 134 153 135 154 ## exception hierarchies 136 155 ··· 180 199 ``` 181 200 182 201 the wrapper's signature now includes `_filter`. IDEs and schema generators see it. 202 + 203 + from [pdsx/mcp/filterable.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/mcp/filterable.py) 183 204 184 205 ## argparse for CLIs 185 206
languages/python/project-setup.md languages/python/ecosystem/project-setup.md
-56
languages/python/pydantic.md
··· 1 - # pydantic 2 - 3 - pydantic is a library, not the language. but it's become foundational enough that it's worth understanding. 4 - 5 - ## the core idea 6 - 7 - python's type hints don't do anything at runtime by default. `def foo(x: int)` accepts strings, floats, whatever - the annotation is just documentation. 8 - 9 - pydantic makes them real. define a model with type hints, and pydantic validates and coerces data to match: 10 - 11 - ```python 12 - from pydantic import BaseModel 13 - 14 - class User(BaseModel): 15 - name: str 16 - age: int 17 - 18 - user = User(name="alice", age="25") # age coerced to int 19 - user = User(name="alice", age="not a number") # raises ValidationError 20 - ``` 21 - 22 - this is why pydantic shows up everywhere in python - it bridges the gap between python's dynamic runtime and the desire for validated, typed data. 23 - 24 - ## settings from environment 25 - 26 - the most common use: replacing `os.getenv()` calls with validated configuration. 27 - 28 - ```python 29 - from pydantic_settings import BaseSettings 30 - 31 - class Settings(BaseSettings): 32 - database_url: str 33 - debug: bool = False 34 - 35 - settings = Settings() # reads from environment 36 - ``` 37 - 38 - if `DATABASE_URL` isn't set, this fails at import time - not later when you try to connect. that fail-fast behavior catches configuration errors before they become runtime surprises. 39 - 40 - ## when to use what 41 - 42 - pydantic models are heavier than they look - they do a lot of work on instantiation. for internal data you control, python's `dataclasses` are simpler: 43 - 44 - ```python 45 - from dataclasses import dataclass 46 - 47 - @dataclass 48 - class InternalRecord: 49 - id: str 50 - value: float 51 - ``` 52 - 53 - no validation, no coercion, just a class with fields. use pydantic at boundaries (API input, config files, external data) where you need validation. use dataclasses for internal data structures. 54 - 55 - sources: 56 - - [how to use pydantic-settings](https://blog.zzstoatzz.io/how-to-use-pydantic-settings/)
languages/python/tooling.md languages/python/ecosystem/tooling.md
-177
languages/python/typing.md
··· 1 - # typing 2 - 3 - python 3.12 made typing feel native. generics have real syntax, type aliases have a keyword, and you rarely need to import from `typing` anymore. 4 - 5 - ## the basics 6 - 7 - use `|` for unions: 8 - 9 - ```python 10 - def fetch(url: str, timeout: float | None = None) -> dict[str, Any]: ... 11 - ``` 12 - 13 - `from __future__ import annotations` at the top of files lets you reference types before they're defined and avoids some runtime costs. 14 - 15 - ## type aliases 16 - 17 - the `type` statement creates type aliases: 18 - 19 - ```python 20 - type UserId = int 21 - type Handler = Callable[[Request], Response] 22 - type Result[T] = T | Error 23 - ``` 24 - 25 - clearer than `TypeAlias` annotations and supports generics directly. 26 - 27 - ## TypedDict 28 - 29 - for structured dictionaries where you know the shape: 30 - 31 - ```python 32 - from typing import TypedDict 33 - 34 - class RecordResponse(TypedDict): 35 - uri: str 36 - cid: str | None 37 - value: dict[str, Any] 38 - 39 - def get_record(uri: str) -> RecordResponse: 40 - ... 41 - ``` 42 - 43 - better than `dict[str, Any]` because the structure is documented and checked. 44 - 45 - ## Annotated 46 - 47 - add metadata to types, especially useful with pydantic: 48 - 49 - ```python 50 - from typing import Annotated 51 - from pydantic import Field 52 - 53 - FilterParam = Annotated[ 54 - str | None, 55 - Field(description="jmespath expression to filter results"), 56 - ] 57 - 58 - def list_records(collection: str, _filter: FilterParam = None) -> list[dict]: 59 - ... 60 - ``` 61 - 62 - the metadata travels with the type. frameworks like fastmcp use it for schema generation. 63 - 64 - ## Protocol 65 - 66 - duck typing with type safety. define what methods something needs, not what class it is: 67 - 68 - ```python 69 - from typing import Protocol, runtime_checkable 70 - 71 - @runtime_checkable 72 - class Closeable(Protocol): 73 - def close(self) -> None: ... 74 - 75 - def cleanup(resource: Closeable) -> None: 76 - resource.close() 77 - ``` 78 - 79 - `@runtime_checkable` lets you use `isinstance()` checks. 80 - 81 - ## generics 82 - 83 - 3.12 introduced native syntax for generics. no more `TypeVar`: 84 - 85 - ```python 86 - def first[T](items: list[T]) -> T | None: 87 - return items[0] if items else None 88 - ``` 89 - 90 - with bounds: 91 - 92 - ```python 93 - from pydantic import BaseModel 94 - 95 - def parse[M: BaseModel](data: dict, model: type[M]) -> M: 96 - return model.model_validate(data) 97 - ``` 98 - 99 - the `[T]` declares the type parameter inline. `[M: BaseModel]` constrains it. this is real syntax, not a workaround. 100 - 101 - ## ParamSpec 102 - 103 - for decorators that preserve function signatures: 104 - 105 - ```python 106 - from typing import ParamSpec, TypeVar, Callable, Awaitable 107 - from functools import wraps 108 - 109 - P = ParamSpec("P") 110 - R = TypeVar("R") 111 - 112 - def logged(fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]: 113 - @wraps(fn) 114 - async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: 115 - print(f"calling {fn.__name__}") 116 - return await fn(*args, **kwargs) 117 - return wrapper 118 - ``` 119 - 120 - this preserves the original function's parameter types in the wrapper. 121 - 122 - ## overload 123 - 124 - when return type depends on input: 125 - 126 - ```python 127 - from typing import overload, Literal 128 - 129 - @overload 130 - def fetch(url: str, raw: Literal[True]) -> bytes: ... 131 - @overload 132 - def fetch(url: str, raw: Literal[False] = False) -> dict: ... 133 - 134 - def fetch(url: str, raw: bool = False) -> bytes | dict: 135 - response = httpx.get(url) 136 - return response.content if raw else response.json() 137 - ``` 138 - 139 - IDEs and type checkers know that `fetch(url, raw=True)` returns `bytes`. 140 - 141 - ## TYPE_CHECKING 142 - 143 - avoid runtime import costs for types only needed for hints: 144 - 145 - ```python 146 - from typing import TYPE_CHECKING 147 - 148 - if TYPE_CHECKING: 149 - from heavy_module import ExpensiveClass 150 - 151 - def process(item: "ExpensiveClass") -> None: 152 - ... 153 - ``` 154 - 155 - the string quote is needed because the import doesn't happen at runtime. 156 - 157 - ## import organization 158 - 159 - ```python 160 - """module docstring.""" 161 - 162 - from __future__ import annotations 163 - 164 - import stdlib_module 165 - from typing import TYPE_CHECKING 166 - 167 - from third_party import thing 168 - 169 - from local_package import helper 170 - 171 - if TYPE_CHECKING: 172 - from expensive import Type 173 - ``` 174 - 175 - sources: 176 - - [pdsx/src/pdsx/_internal/types.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/types.py) 177 - - [fastmcp/src/fastmcp/tools/tool.py](https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py)
languages/python/uv.md languages/python/ecosystem/uv.md