TheSkillz

Python Project Standards

uv, ruff, strict typing and enforced dependency direction

TheSkillz Team TheSkillz Team No reviews yet0 installsv1.0.0
Scan passed · 100/100Human reviewedOfficial · TheSkillz
☆ Star 0

Sets up uv with a src layout, the ruff selection that catches real complexity rather than every rule plus noqa, and strict mypy or pyright with no bare Any. Adds import-linter layer and independence contracts so dependency direction is enforced in CI instead of documented in a README.

SKILL.md

Python Project Standards

Python will let a service become a ball of mud faster than any other language on this list, because nothing stops one module importing another. The discipline that matters is not style — ruff settles that in a second — it is dependency direction, and it has to be enforced by a tool or it will not hold.

Toolchain

  • uv for environments, dependency resolution and locking. uv sync is reproducible; pip install -r requirements.txt is not.
  • pyproject.toml as the single config file. No setup.py, no setup.cfg, no .flake8.
  • ruff for both linting and formatting. It replaces flake8, isort, black, pyupgrade and most plugins, and it is fast enough to run on save.
  • mypy or pyright in strict mode. Pick one and put it in CI. A type checker that only runs in an editor checks nothing.
  • Pin the Python version in requires-python and in CI, and make them match.

Layout

Use a src layoutsrc/<package>/ — so tests import the installed package rather than the working directory. Flat layout silently tests files that would not ship.

Then choose the internal shape deliberately:

  • Flat modules for a library or anything under roughly ten modules. Feature directories at that size are ceremony.
  • Feature packagessrc/app/<feature>/{router,service,repository,schemas}.py — for a service with multiple entry points and more than one contributor. The unit that owns its own data access is the feature, not the layer.

Do not organise by technical layer at the top level (models/, views/, services/). It scales badly: every change touches every directory, and nothing tells you what the application does.

__init__.py stays empty in feature packages. Re-exporting the whole subtree from __init__ creates import cycles and makes every import pull the world.

The ruff rules that matter

Enabling every rule and then scattering noqa teaches the team to ignore the linter. Select deliberately:

  • E, F, W, I — the baseline plus import sorting.
  • C901 (complexity), PLR0915 (too many statements), PLR0912 (too many branches), PLR0913 (too many arguments). These are the budget rules and they are the point.
  • TID252 to ban relative imports beyond the current package, which is how cycles start.
  • B (bugbear) for mutable default arguments and the other genuine traps.
  • RUF100 so an unused noqa is itself an error — otherwise suppressions accumulate forever.
  • ARG for unused arguments, PTH to push off os.path, ASYNC if the codebase is async.

Set [tool.ruff.lint.pylint] max-statements = 50 and keep files under 300 lines as a budget: over budget, a file either splits or carries a one-line header saying why it does not.

Typing

  • Strict mode on. disallow_untyped_defs, warn_return_any, no_implicit_optional.
  • No bare Any. If a shape is genuinely unknown, use object and narrow, so the checker forces the check.
  • No # type: ignore without an error code and a reason# type: ignore[arg-type] # lib is untyped. Set enable_error_code = ["ignore-without-code"].
  • Protocol over ABC for anything you own. Structural typing means the implementation does not have to import the abstraction, which keeps the dependency arrow pointing the right way.
  • pydantic at the boundary only — request bodies, config, external API responses. Inside the domain use dataclasses or plain types; pydantic validation in a hot loop is a real cost.
  • from __future__ import annotations at the top of every module on Python 3.11.

Enforce dependency direction

This is the piece almost no Python repo has, and the reason services rot. Add import-linter contracts to pyproject.toml and run lint-imports in CI:

  • A layers contract for the vertical direction: api may import service, service may import repository, and never the reverse.
  • An independence contract listing the feature packages, so no feature can import a sibling.
  • A forbidden contract keeping the domain free of framework imports — no fastapi or django inside src/app/domain.

A contract that fails the build is worth more than a paragraph in a README that everyone has agreed to.

Verification gate

  • uv sync --frozen succeeds — the lockfile is current.
  • ruff check . and ruff format --check . clean.
  • mypy src/ (or pyright) clean in strict mode, with no rule relaxed to get there.
  • lint-imports passes and no contract was weakened.
  • pytest passes; no test was skipped or marked xfail to reach green.
  • Paste the command output rather than summarising it.

Reviews

Sign in to leave a review.

  • Be the first to review this skill.

More in coding