Skip to content

Code Quality Tools

Clean code principles are enforced by humans during review — but code quality tools catch violations automatically, before review even starts.


Linters

A linter performs static analysis — it reads source code without running it and reports violations of style rules, complexity limits, or bug-prone patterns.

Python

Tool What It Checks
ruff Style, imports, complexity, unused vars — replaces flake8, isort, pyupgrade
mypy Type annotations — catches type mismatches before runtime
wemake-python-styleguide Strict style: cognitive complexity, nesting depth, naming conventions
bandit Security — detects hardcoded passwords, unsafe eval, SQL injection patterns
# pyproject.toml
[tool.ruff]
line-length = 88
select = ["E", "F", "W", "C90", "I", "N", "UP", "S"]

[tool.mypy]
strict = true
disallow_untyped_defs = true

Formatters

Formatters rewrite code to enforce consistent style. Unlike linters, they don't report — they fix. Non-negotiable in any shared codebase.

Tool Note
ruff format Replaces black — fast, opinionated, same output
black Original formatter, widely adopted

Type Checkers

Type checking bridges the gap between dynamic languages and compile-time safety.

# mypy catches this before runtime
def get_user(user_id: str) -> dict:
    return db.find(user_id)

result = get_user(42)  # error: Argument 1 has incompatible type "int"; expected "str"

Run as part of CI — treat type errors as build failures, not warnings.


Complexity Metrics

High complexity = hard to test, hard to understand, easy to break.

Metric Tool Threshold
Cyclomatic complexity ruff (C90), radon > 10 is a warning
Cognitive complexity wemake-python-styleguide > 12 is a warning
Lines per function Any linter > 30 is a smell
Arguments per function wemake > 5 is a smell

Pre-commit Hooks

Pre-commit hooks run checks before a commit is created. If any check fails, the commit is rejected. Developers fix the issue, then commit again.

Setup

pip install pre-commit
pre-commit install       # installs hook into .git/hooks/pre-commit

.pre-commit-config.yaml

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.9.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-merge-conflict
      - id: detect-private-key

Run manually on all files:

pre-commit run --all-files

CI Integration

Pre-commit stops bad code locally. CI stops it at the repository level — even if a developer bypasses hooks with --no-verify.

# .github/workflows/quality.yml
name: Code Quality

on: [push, pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3

      - name: Lint
        run: uv run ruff check .

      - name: Format check
        run: uv run ruff format --check .

      - name: Type check
        run: uv run mypy src/

      - name: Security scan
        run: uv run bandit -r src/ -ll

Coverage Enforcement

Test coverage without enforcement drifts downward over time. Enforce a minimum threshold in CI:

uv run pytest --cov=src --cov-fail-under=95 --cov-report=term-missing

Coverage alone does not guarantee quality — a test that calls a function without asserting anything gives 100% coverage and zero confidence. Use coverage as a floor, not a goal.


Tool Execution Order

The recommended order — fast checks first, slow checks last:

Format (ruff format)     → 0.1s  — fix style
Lint (ruff check)        → 0.3s  — catch errors
Type check (mypy)        → 2–5s  — catch type issues
Security (bandit)        → 1–3s  — catch security issues
Tests + coverage         → 10s+  — validate behavior

Run format + lint in pre-commit. Run everything in CI.