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.
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:
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:
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 @classmethods named from_*:
@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:
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:
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:
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:
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.
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:
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
- plyr-python-client
- docket
- FastMCP #5255 and #5256, reviewed 2026-09-24