# patterns class design, decorators, error handling, and other structural patterns. ## private internals keep implementation details in `_internal/`; `__init__.py` re-exports the public API: ``` src/mypackage/ ├── __init__.py # public API ├── cli.py # entry point └── _internal/ # implementation details ``` users import from the package, not from internals. ## dataclasses for DTOs for the pydantic-vs-dataclass boundary (validate at edges, plain dataclasses inside), see [pydantic](../ecosystem/pydantic/README.md). ## base classes with parallel implementations when you need both sync and async, put shared logic in a base class and diverge only in the subclasses: ```python class _BaseClient: def __init__(self, *, token: str | None = None): self._token = token or get_settings().token class Client(_BaseClient): def get(self, url: str) -> dict: ... class AsyncClient(_BaseClient): async def get(self, url: str) -> dict: ... ``` ## fluent interfaces chainable methods mutate state and return `self`: ```python def volume(self, level: float) -> "Track": self._effects.append(f"volume={level}") return self track.volume(0.8).lowpass(600).fade_in(0.5) ``` ## factory classmethods alternative constructors as `@classmethod`s named `from_*`: ```python @classmethod def from_uri(cls, uri: str) -> URIParts: repo, collection, rkey = uri.removeprefix("at://").split("/") return cls(repo=repo, collection=collection, rkey=rkey) ``` keeps parsing/derivation logic out of `__init__`, which stays a plain field assignment. ## keyword-only arguments a bare `*` forces callers to name everything after it: ```python def batch_create(client, collection, records, *, concurrency: int = 10, fail_fast: bool = False): ... ``` call sites stay legible and reordering defaults is not a breaking change. ## custom exceptions put remediation in the message, not just the diagnosis: ```python class AuthenticationRequired(Exception): def __init__(self, operation: str = "this operation"): super().__init__( f"{operation} requires authentication.\n\n" "Set ATPROTO_HANDLE and ATPROTO_PASSWORD environment variables." ) ``` give an app a base exception class so callers can catch broadly or narrowly: ```python class AppError(Exception): ... class ValidationError(AppError): ... class ResourceError(AppError): ... ``` ## argparse for CLIs argparse is stdlib. subparsers with aliases give unix-style commands; dispatch with tuple membership to cover aliases: ```python subparsers = parser.add_subparsers(dest="command") list_parser = subparsers.add_parser("list", aliases=["ls"]) ... if args.command in ("list", "ls"): return await cmd_list(args.collection, args.limit) ``` async entry point: `def main() -> NoReturn: sys.exit(asyncio.run(async_main()))`. ## module-level singletons instantiate once at module level, import everywhere (`console = Console()`). for settings, use the cached-factory form — see [pydantic settings](../ecosystem/pydantic/settings.md). ## finalizers and partial construction `__del__` can run on an object whose `__init__` never completed. This is common when a subclass validates input before calling `super().__init__()`: Python has already allocated the instance, so a raised validation error still leaves an object for garbage collection. A finalizer must therefore treat every attribute established during initialization as optional: ```python def __del__(self) -> None: stop_event = getattr(self, "_stop_event", None) if stop_event is not None: stop_event.set() ``` Do not manufacture missing cleanup state in the finalizer. If construction did not acquire the resource, there is nothing to release. The guard also prevents an unraisable finalizer exception from obscuring the constructor error that the caller needs to diagnose. ## sources - [pdsx](https://github.com/zzstoatzz/pdsx) - [plyr-python-client](https://github.com/zzstoatzz/plyr-python-client) - [docket](https://github.com/chrisguidry/docket) - FastMCP [#5255](https://github.com/PrefectHQ/fastmcp/issues/5255) and [#5256](https://github.com/PrefectHQ/fastmcp/pull/5256), reviewed 2026-09-24