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, bareexcept. These are the classic Python traps. See ../02_python_core/tricky_questions/01_mutable_default_arguments.md.UP(pyupgrade) — rewrites old syntax for yourtarget-version. Free modernisation.S(bandit) — security:subprocesswithshell=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 inpyproject.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
strictglobally 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.