backend / code quality / 02_linters.md

Linters and static analysis

5 interview angles 3 min read source

Linters and static analysis

Catching bugs before review. In 2026 the Python answer consolidated onto one tool.

Ruff

Ruff implements 800+ rules from flake8, isort, pyupgrade, bandit, pylint and others in a single Rust binary. It’s used by FastAPI, LangChain, HuggingFace and most major Python projects.

[tool.ruff.lint]
select = [
    "E", "W",    # pycodestyle
    "F",         # pyflakes - real bugs: unused imports, undefined names
    "I",         # isort - import sorting
    "UP",        # pyupgrade - modernise syntax for the target version
    "B",         # bugbear - likely bugs
    "S",         # bandit - security
    "SIM",       # simplify
    "RUF",       # ruff-specific
]
ignore = ["E501"]           # line length is the formatter's job

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]        # assert is fine in tests

The rule sets worth enabling deliberately:

  • F (pyflakes) — actual bugs. Undefined names, unused imports. Non-negotiable.
  • B (bugbear) — the good stuff: mutable default arguments, loop-variable capture in closures, bare except. These are the classic Python traps. See ../02_python_core/tricky_questions/01_mutable_default_arguments.md.
  • UP (pyupgrade) — rewrites old syntax for your target-version. Free modernisation.
  • S (bandit) — security: subprocess with shell=True, weak hashes, hardcoded credentials.

Type checking is a separate job

A linter checks patterns; a type checker checks types across call boundaries. You want both.

[tool.mypy]
python_version = "3.14"
strict = true
warn_unreachable = true

[[tool.mypy.overrides]]
module = "untyped_dependency.*"
ignore_missing_imports = true

mypy is the established, safe answer. pyright is faster with better inference and powers Pylance. ty (from Astral, Ruff’s authors) is the newer Rust-based entrant — worth watching, not yet the default recommendation.

Adopting strict typing gradually is the practical question. Start permissive, add per-module overrides as code gets typed, and ratchet up. Turning on strict across a large untyped codebase produces thousands of errors and gets reverted within a week. See 03_type_checking_strategy.md.

The 2026 toolchain

One config file — pyproject.toml — holds uv, Ruff, mypy, pytest, coverage and build settings. setup.cfg, setup.py, .flake8 and mypy.ini are legacy.

uv run ruff check .
uv run ruff format --check .
uv run mypy src/
uv run pytest

What to enforce, and what not to

Enforce in CI Leave as a warning
pyflakes errors complexity thresholds
security rules docstring coverage
type errors in typed modules naming beyond the basics
formatting subjective style rules

A linter that produces noise gets ignored. Enable rules that catch bugs; be sparing with rules that encode taste. A codebase with 4,000 warnings has the same practical value as one with none.

Beyond linting

Tool Catches
pip-audit / uv audit known CVEs in dependencies
bandit (or Ruff S) security anti-patterns
Semgrep custom cross-file patterns
CodeQL deep dataflow analysis
vulture dead code

Dependency auditing belongs in CI and on a schedule — a CVE published after your last commit is still your problem. See ../27_cicd/06_secrets_and_supply_chain.md.

Interview angle

  • “What’s your Python linting setup?” — Ruff for lint and format with an explicit rule selection (F, B, UP, S, I, SIM), mypy for types, everything in pyproject.toml, enforced in CI. Ruff consolidated flake8 plus isort plus pyupgrade plus bandit into one fast binary.
  • “Linter or type checker?” — both, different jobs. A linter finds patterns and likely bugs within a file; a type checker verifies types across call boundaries.
  • “How do you add strict typing to a large untyped codebase?” — gradually, with per-module overrides, ratcheting strictness up as modules get typed. Enabling strict globally produces thousands of errors and gets reverted.
  • “Which lint rules actually matter?” — pyflakes for real bugs, bugbear for the classic Python traps like mutable defaults and late-binding closures, bandit for security. Be sparing with taste-based rules; a noisy linter gets ignored entirely.
  • “What else runs in CI besides lint and tests?” — a dependency vulnerability audit, scheduled as well as per-commit, because CVEs get published after your code stops changing.