Python Conventions

Type Hints

  • Add type hints to all function signatures — parameters and return types
  • Use T | None (3.10+) instead of Optional[T] — cleaner syntax
  • Use TypedDict for dictionary shapes, Protocol for structural typing
  • Use built-in types for annotations: list[int], dict[str, float], tuple[str, int] — not their typing module equivalents
  • Run mypy or pyright in CI — treat type errors as bugs; they catch real issues before runtime
  • Use @override decorator (3.12+) on methods that override a parent — type checkers flag if the parent method is removed or renamed
# BAD: no type hints
def get_user(id):
    return db.find(id)

# GOOD: typed signature
def get_user(user_id: str) -> User | None:
    return db.find(user_id)

Generic Types (Python 3.12+)

  • Use the new type parameter syntax instead of TypeVar for generics — cleaner, less boilerplate
  • Use the type statement for type aliases: type Vector = list[float]
# BAD (pre-3.12): manual TypeVar
from typing import TypeVar, Generic
T = TypeVar("T")
class Stack(Generic[T]): ...

# GOOD (3.12+): built-in syntax
class Stack[T]: ...

Data Models

  • Use dataclasses for simple data containers
  • Use Pydantic for models with validation (API inputs, config files)
  • Prefer immutable models: frozen=True on dataclasses — prevents accidental mutation after creation
  • Never use plain dicts for structured data — define a model; dicts have no validation and no IDE support

Resource Management

  • Use context managers (with) for files, connections, locks
  • Implement __enter__/__exit__ for custom resources
  • Close database connections in finally blocks
  • Use contextlib.asynccontextmanager for async resources

Structural Pattern Matching (3.10+)

  • Use match/case for complex branching on data structure shape — cleaner than if/elif chains for type dispatching
  • Use guard clauses (case X if condition) for conditional matching
  • Prefer match/case over isinstance chains when destructuring dataclasses or tuples

Error Handling

  • Define custom exception classes for domain errors
  • Catch specific exceptions — never bare except:; bare except catches KeyboardInterrupt and SystemExit
  • Use logging module — never print() for operational output; logging supports levels, formatting, and routing
  • Include context in error messages: what failed, with what input
  • Use except* to handle ExceptionGroup (3.11+) — required when using asyncio.TaskGroup, which raises ExceptionGroup on multi-task failures

Async Patterns

  • Use asyncio.TaskGroup (3.11+) for structured concurrency — tasks are automatically cancelled on failure, unlike gather()
  • Prefer async with for async resource management (database connections, HTTP sessions)
  • Set timeouts on all async operations with asyncio.timeout()

Testing

  • Use pytest with fixtures for test setup and teardown
  • Name tests descriptively: test_<action>_<condition>_<expected>
  • Use parametrize for testing multiple inputs against same logic
  • Mock only external dependencies (APIs, databases, file system)

Package Management

  • Use uv for dependency management — 10-100x faster than pip, handles virtualenvs, lockfiles, and Python versions
  • Prefer pyproject.toml over setup.py/setup.cfg for project metadata and dependency declaration

Formatting & Style

  • Use ruff for both linting and formatting — single Rust binary replaces flake8, isort, black, and 50+ other tools
  • Configure ruff in pyproject.toml — one configuration file for all rules
  • Use f-strings for string formatting — no % or .format()
  • Use pathlib.Path over os.path for file operations — cleaner API and cross-platform by default
  • Prefer list/dict/set comprehensions over manual loops for transformations