Stack conventions
Python
Modern Python 3.10+ services, libraries and scripts, typed and tested with pytest.
Modern Python 3.10+ services, libraries and scripts, typed and tested with pytest.
Used for: python.
Principles
- Prefer clear, explicit code over clever code; readability is the first requirement.
- Add type hints to every new public function, method and module-level constant, and keep them truthful.
- Keep functions small and side effects at the edges; pass dependencies in rather than importing global state.
- Raise specific exceptions with a useful message; never silence an exception without handling or logging it.
- Use the standard library first and add a third-party dependency only when it removes real complexity.
- Make data shapes explicit with dataclasses, TypedDict or pydantic models instead of loose dicts.
- Write tests with the change, using pytest, that assert behaviour rather than implementation details.
- Treat all external input as untrusted; validate it at the boundary and never build commands or SQL from strings.
- Manage every resource (files, connections, locks, temporary directories) with a context manager so it is released on every path.
- Use pathlib.Path for filesystem paths and the logging module for diagnostics; print is for a command-line program's intended output only.
- Give every network call, subprocess and lock acquisition a timeout; nothing external may block forever.
Conventions
Accessibility
Concurrency
- Do not block the event loop in async code; use async libraries or run blocking work in a thread.
- Use concurrent.futures for parallel I/O in synchronous code and processes for CPU-bound work.
- Protect shared mutable state with locks or avoid sharing it; prefer passing messages.
- In asyncio, run concurrent tasks under asyncio.TaskGroup (3.11+) with asyncio.timeout, and never drop the reference to a task created with create_task.
Dependencies
- Add dependencies to the file the repository uses (pyproject.toml, requirements*.txt) with a compatible version range.
- Pin exact versions in lock files or constraints for applications, not in library metadata.
- Prefer well-maintained libraries and avoid adding one for a few lines of code.
- Use the lock tool the repository already uses (uv.lock, poetry.lock, pip-tools requirements) and update it in the same change as the dependency.
Documentation
- Give every public module, class and function a docstring that says what it does and what it returns or raises.
- Explain why in comments; the code already says what.
- Update the README or docs when behaviour, configuration or commands change.
Errors
- Catch the narrowest exception type that the code can actually handle.
- Chain exceptions with raise ... from err when translating them, so the cause is kept.
- Define a small exception hierarchy per package when callers need to tell failures apart.
- Do not use assert for runtime validation; asserts are removed under -O.
- Use timezone-aware datetimes (datetime.now(timezone.utc)) for timestamps; naive datetimes cause off-by-hours bugs across hosts.
Logging
- Get a module logger with logging.getLogger(name); never configure logging at import time in a library.
- Use lazy formatting (log.info("saved %s", item_id)) rather than f-strings in log calls.
- Never log secrets, tokens, passwords or full personal data.
Naming
- Use snake_case for functions, variables and modules, PascalCase for classes and UPPER_SNAKE_CASE for constants.
- Prefix internal helpers with a single underscore; do not use double underscores for privacy.
- Name booleans as questions (is_ready, has_access) and functions after what they return or do.
Performance
- Choose the right data structure (set or dict lookups instead of list scans in loops).
- Avoid repeated database or HTTP calls inside loops; batch or prefetch.
- Measure with a profiler before optimising and keep the readable version unless the gain is real.
- Stream with generators and itertools instead of building large intermediate lists; cache pure repeated computations with functools.cache or lru_cache.
Security
- Use parameterised queries or the ORM's query builder; never format SQL with user input.
- Call subprocess with a list of arguments and shell=False; never pass untrusted text to a shell.
- Use secrets for tokens and hmac.compare_digest for comparing them; never random for security.
- Load YAML with yaml.safe_load and never unpickle data from an untrusted source.
- Read credentials from the environment or a secret store, never from source code.
- Set explicit timeouts on HTTP clients (requests has none by default) and verify TLS; never pass verify=False outside tests.
Structure
- Keep the repository's package layout; place new modules next to the code they belong with.
- Declare project metadata and tool configuration in pyproject.toml when the repository uses it.
- Keep import order standard library, third party, local, and avoid wildcard imports.
- Guard executable scripts with if name == "main" and keep logic importable.
- For new packages use the src/ layout, PEP 621 [project] metadata in pyproject.toml and console entry points in [project.scripts] instead of setup.py.
- Use modern annotation syntax (list[str], dict[str, int], X | None); add from future import annotations where forward references or older supported versions need it, and keep the choice consistent per package.
Testing
- Write pytest tests named after the behaviour they check, in the repository's tests directory.
- Use fixtures and tmp_path for setup, and monkeypatch or fakes rather than patching internals.
- Cover failure paths and edge cases, not only the happy path.
- Keep tests deterministic; no network, no real clock or sleep without control.
- Use @pytest.mark.parametrize (with ids) for input variations instead of loops or copy-pasted tests.
- Keep shared fixtures in conftest.py with the narrowest scope that works; assert errors with pytest.raises(..., match=...).
- Unit-test logic in isolation with fakes; add integration tests (real database in a container or a temporary SQLite file, the app's test client for HTTP) where the change crosses a boundary.
- Done means tested: new or changed behaviour ships with tests in the same change, and a fixed bug gets a regression test that fails without the fix.
Review checklist
- Do all new public functions and methods have accurate type hints?
- Does the change follow the repository's existing structure, naming and formatting?
- Are exceptions specific, chained where translated, and never silently swallowed?
- Is all external input validated at the boundary before use?
- Are SQL queries parameterised and subprocess calls made without a shell?
- Are secrets kept out of code, logs and error messages?
- Do new or changed behaviours have pytest tests, including failure paths?
- Are tests deterministic, with no network access or real sleeps?
- Are files, connections and locks closed through context managers?
- Is logging done through a module logger with lazy formatting?
- Are new dependencies necessary and declared in the repository's dependency file?
- Are public modules, classes and functions documented with docstrings?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
ruff |
lint | ruff | no |
ruff-format |
format | ruff | no |
black |
format | black | no |
mypy |
typecheck | mypy | no |
pyright |
typecheck | pyright | no |
bandit |
security | bandit | no |
pip-audit |
security | pip-audit | no |
gitleaks |
security | gitleaks | no |