SuperAPI AI Directory

python-coding-standards

Coding & development

View repository

Updated 10-15 17:31

About this skill

python-coding-standards

Standards for writing and editing Python, so every file in a project reads consistently regardless of which session wrote it. Every piece of runnable Python — a demo script, a one-off, an example saved to a file, or code left in the chat to copy-paste and run — is real code the rules below apply to. The one exemption is a snippet written purely to illustrate a concept in prose, with no runnable entry point (e.g. what a decorator looks like); if you can't tell whether something has a runnable entry point, assume it does.

Core Rules

  1. [NEVER VIOLATE] Functions the user is going to run get complete type hints. Every parameter and the return type. When editing an existing file, this applies to the function you're adding or touching; the rest of the file keeps its signatures as they are unless asked.
  1. [NEVER VIOLATE] No global variables. Use instance attributes, closures, or dependency injection instead. The smell this rule targets is a module-level or class-level name that gets mutated after it's defined — whether one function touches it (the global keyword is the textbook case) or several. A name never reassigned or mutated, scalar or container (MAX_RETRIES = 3, DEFAULT_HEADERS = {...}), is a constant and fine, and so is a module-level logger = logging.getLogger(__name__).

  2. Use logging, never print(), for anything diagnostic. print() is acceptable only for a CLI tool's actual user-facing output — the thing the program exists to print, not a debug trace left behind. A standalone script calls logging.basicConfig(level=logging.INFO, format=...) once at its entry point (if __name__ == "__main__":); without an explicit level=, the root logger defaults to WARNING and logger.info silently produces no output. A module meant to be imported leaves basicConfig to its importer.

  3. Wrap in try/except, and log the failure, any I/O that can fail for reasons your own code doesn't control: HTTP calls, DB connections, subprocess calls, and file reads/writes. This doesn't cover writing to stdout/stderr (Rule 3's print() case) — that doesn't fail for external reasons. Log enough to diagnose without a repro — what was called, and the actual exception, not just "failed".

  4. Comment the WHY, not the WHAT. Only add a comment when it explains something the code itself can't — a hidden constraint, a non-obvious trade-off, a workaround for a specific bug.

  5. Tooling: .venv + requirements.txt, PEP 8, Black. Pin every dependency in requirements.txt with PEP 440's compatible-release operator (~=major.minor, e.g. numpy~=2.4) — locks the major version, lets minor/patch float to latest automatically on every install. Not ==exact-version (blocks every patch/security update until a human manually bumps it) and not a bare >= (no ceiling — a future major-version bump gets pulled in silently, defeating the point of pinning). ^= isn't valid pip/PEP 440 syntax — that's npm/Cargo/Poetry, and pip rejects it as an invalid requirement. A calendar-versioned package (e.g. pytz, whose releases look like 2025.2) follows the same rule mechanically — pin ~=2025.2 and a same-year bump (2025.3) is picked up automatically, but a new year (2026.1) is not and needs a manual pin update.

    ⚠️ A pin cannot help when the platform has no wheel at all: pip says nothing and falls back to building from source, which on a slow machine looks like a hang lasting tens of minutes. Hit twice with compiled packages (opencv-python, onnxruntime): their newer releases ship wheels only for macOS 13+, installed on an Intel Mac stuck on macOS 12; and the same packages in an Alpine (musl) container, where no musllinux wheel exists. Make the install fail fast instead:

    .venv/bin/python -m pip install --only-binary :all: -r requirements.txt
    

    No matching distribution found for <package> names the one with no wheel. Fix it per environment, keeping the shared requirements.txt untouched: on the old machine, first install that package with a < upper bound at the last release that still ships a wheel for it, then install the rest; in a container, switch to a glibc base image (Debian) rather than pinning.

  6. Encoding: UTF-8, declared only for a non-UTF-8 file. Save .py files as UTF-8, with no encoding line in the header: Python 3 already decodes source as UTF-8 (PEP 3120), so a # -*- coding: utf-8 -*- line changes nothing (Ruff flags it as UP009). A PEP 263 declaration earns its place only in a file that must be saved in another encoding, e.g. # -*- coding: latin-1 -*-, on line 1 or 2. An existing file that already carries the UTF-8 line keeps it: removing it is diff noise, not a fix.

Testing

  • Every piece of logic you write or change — a new script, a new feature, or a bug fix — needs a test that exercises its business logic — the behaviour a caller depends on, not the fact that the function ran. [NEVER VIOLATE] Never hardcode a return value, or assert a tautology, just to make a test pass — this applies to a probe just as much as a formal test.
  • Test infra (e.g. pytest) already exists → write the test first, then the implementation.
  • No test infra, but the code lives in an existing project (a repo, a package — somewhere with a future) → set up pytest rather than inventing a bespoke runner.
  • No test infra, and the code really is a standalone one-off with no project to land in → at minimum, write a runnable probe: a short standalone snippet that feeds the new code a known input and checks the actual output against what you expect.
  • Project already has its own hand-rolled test/probe setup → write the test in it. If switching to pytest looks worth the churn, suggest it to the user.
  • A test that passes doesn't prove it catches anything — verify it by breaking the code it's supposed to guard and confirming the test goes red, then restore the fix. A common way to write an accidental tautology: pytest's caplog fixture attaches to the root logger, so it silently captures nothing against a logger configured with propagate=False — a logging assertion built on caplog there stays green even after the logged level regresses. Assert against a handler attached directly to that logger instead.
  • An isolated test needs every side-effecting boundary mocked or redirected at once — a real database, files it writes (including incidental ones, like an audit-log dump), and any logger the code under test configures. Missing just one still lets the test corrupt production data or files on a real run.
  • Passing tests (and a clean ast.parse) don't prove a script actually runs — a NameError from a leftover stale reference, for instance, shows up only when the entry point is actually executed. After editing a module another script imports, run that script for real at least once.

Design Docs

  • Design docs, if the project keeps them, go wherever that project's existing documentation convention already puts them. Don't invent a new location (e.g. an ad hoc _doc/ folder) alongside a project that already has one.