Code Quality & Tooling

0/4 in this phase0/54 across the roadmap

📖 Concept

Code quality tools automate the enforcement of style guides, catch bugs before they reach production, and ensure consistency across teams. In the Python ecosystem, the tooling landscape has converged around a few key categories: linters (find bugs and style violations), formatters (auto-fix style), import sorters, and pre-commit hooks (run checks before every commit).

Linters:

Tool Purpose Speed Notes
pylint Comprehensive linter, type checks, code smells Slow Most thorough, can be noisy
flake8 Style (PEP 8) + logical errors Medium Lightweight, extensible with plugins
ruff All-in-one linter + formatter Extremely fast Written in Rust, replaces flake8/isort/pylint rules

Ruff has rapidly become the standard in 2024+ because it implements 800+ lint rules (from flake8, pylint, isort, pyupgrade, and more) at 10-100x the speed of traditional Python-based tools. It is a drop-in replacement for most linting setups.

Formatters:

  • black — opinionated, deterministic formatter ("any color you like, as long as it's black"). Minimal configuration, ends style debates.
  • ruff format — Rust-based formatter compatible with Black's style. Faster alternative.
  • autopep8 — PEP 8 fixer (less opinionated than Black)
  • yapf — Google's formatter (configurable, but less popular now)

Import sorting:

  • isort — sorts imports into sections (stdlib, third-party, local) with configurable profiles
  • ruff — includes isort-compatible import sorting (ruff check --select I --fix)

Pre-commit hooks run automated checks before each git commit, preventing bad code from entering the repository:

  • pre-commit framework — manages hook installation and execution
  • Hooks run only on staged files (fast feedback)
  • CI should also run the same checks as a safety net

Configuration: Modern Python projects centralize tool configuration in pyproject.toml, avoiding a proliferation of dotfiles (.pylintrc, .flake8, setup.cfg). Ruff, Black, isort, pytest, and mypy all support pyproject.toml sections.

Type checking with mypy or pyright catches type errors statically. Combined with linting and formatting, a well-configured toolchain catches the majority of bugs before tests even run.

💻 Code Example

codeTap to expand ⛶
1# ============================================================
2# 1. RuffModern all-in-one linter + formatter
3# ============================================================
4# Install: pip install ruff (or uv add ruff)
5
6# pyproject.toml configuration:
7# [tool.ruff]
8# target-version = "py312"
9# line-length = 88
10# src = ["src"]
11#
12# [tool.ruff.lint]
13# select = [
14# "E", # pycodestyle errors
15# "W", # pycodestyle warnings
16# "F", # pyflakes
17# "I", # isort
18# "N", # pep8-naming
19# "UP", # pyupgrade
20# "B", # flake8-bugbear
21# "SIM", # flake8-simplify
22# "RUF", # ruff-specific rules
23# "S", # flake8-bandit (security)
24# "C4", # flake8-comprehensions
25# "DTZ", # flake8-datetimez
26# "T20", # flake8-print (no print in production)
27# "PT", # flake8-pytest-style
28# ]
29# ignore = [
30# "E501", # line too long (handled by formatter)
31# "S101", # assert used (we use assert in tests)
32# ]
33#
34# [tool.ruff.lint.per-file-ignores]
35# "tests/**/*.py" = ["S101", "T20"] # allow assert and print in tests
36#
37# [tool.ruff.format]
38# quote-style = "double"
39# indent-style = "space"
40
41# Commands:
42# ruff check . # lint
43# ruff check --fix . # lint with auto-fix
44# ruff format . # format (like black)
45# ruff format --check . # check formatting without changing
46
47
48# ============================================================
49# 2. BlackOpinionated code formatter
50# ============================================================
51# Install: pip install black
52
53# pyproject.toml:
54# [tool.black]
55# line-length = 88
56# target-version = ["py312"]
57# include = '\.pyi?$'
58
59# Before black:
60def messy_function( x,y ,z ):
61 return {"key1":x,"key2":y ,
62 "key3" :z}
63
64result=messy_function(1,2,
65 3 )
66
67# After black formats it:
68def messy_function(x, y, z):
69 return {"key1": x, "key2": y, "key3": z}
70
71
72result = messy_function(1, 2, 3)
73
74# Commands:
75# black . # format all files
76# black --check . # check without modifying
77# black --diff . # show what would change
78
79
80# ============================================================
81# 3. isort — Import sorting
82# ============================================================
83# Install: pip install isort
84
85# pyproject.toml:
86# [tool.isort]
87# profile = "black" # compatible with black formatting
88# src_paths = ["src"]
89# known_first_party = ["myproject"]
90# sections = [
91# "FUTURE",
92# "STDLIB",
93# "THIRDPARTY",
94# "FIRSTPARTY",
95# "LOCALFOLDER",
96# ]
97
98# Before isort:
99# import os
100# from myproject.utils import helper
101# import sys
102# import requests
103# from pathlib import Path
104# from collections import defaultdict
105# import json
106
107# After isort:
108import json
109import os
110import sys
111from collections import defaultdict
112from pathlib import Path
113
114import requests
115
116from myproject.utils import helper
117
118
119# ============================================================
120# 4. pylint — Comprehensive static analysis
121# ============================================================
122# Install: pip install pylint
123
124# pyproject.toml:
125# [tool.pylint.messages_control]
126# disable = [
127# "C0114", # missing-module-docstring
128# "C0115", # missing-class-docstring
129# "R0903", # too-few-public-methods
130# ]
131#
132# [tool.pylint.format]
133# max-line-length = 88
134
135# pylint catches things other linters miss:
136class DatabaseConnection:
137 def __init__(self, host, port):
138 self.host = host
139 self.port = port
140 self._connection = None
141
142 def connect(self):
143 # pylint would warn: attribute '_connection' defined outside __init__
144 # if we added self._new_attr = "oops" here
145 self._connection = f"connected to {self.host}:{self.port}"
146 return self._connection
147
148 def query(self, sql):
149 if not self._connection:
150 raise RuntimeError("Not connected")
151 return f"Executing: {sql}"
152
153
154# ============================================================
155# 5. flake8 — Lightweight PEP 8 + logic checker
156# ============================================================
157# Install: pip install flake8
158
159# .flake8 or setup.cfg (flake8 doesn't support pyproject.toml natively):
160# [flake8]
161# max-line-length = 88
162# extend-ignore = E203, W503
163# per-file-ignores =
164# tests/*.py: S101
165# max-complexity = 10
166
167# Popular flake8 plugins:
168# pip install flake8-bugbear # additional bug checks
169# pip install flake8-comprehensions # better list/dict/set comprehensions
170# pip install flake8-bandit # security checks (wraps bandit)
171
172# Commands:
173# flake8 . # check all files
174# flake8 --statistics # summary of violations
175# flake8 --max-complexity 10 # enforce cyclomatic complexity
176
177
178# ============================================================
179# 6. Pre-commit hooks — Automate checks before every commit
180# ============================================================
181# Install: pip install pre-commit
182
183# .pre-commit-config.yaml:
184# repos:
185# - repo: https://github.com/astral-sh/ruff-pre-commit
186# rev: v0.8.0
187# hooks:
188# - id: ruff
189# args: [--fix]
190# - id: ruff-format
191#
192# - repo: https://github.com/pre-commit/pre-commit-hooks
193# rev: v5.0.0
194# hooks:
195# - id: trailing-whitespace
196# - id: end-of-file-fixer
197# - id: check-yaml
198# - id: check-added-large-files
199# args: [--maxkb=500]
200# - id: check-merge-conflict
201# - id: debug-statements # catches breakpoint() and pdb
202#
203# - repo: https://github.com/pre-commit/mirrors-mypy
204# rev: v1.13.0
205# hooks:
206# - id: mypy
207# additional_dependencies: [types-requests]
208
209# Commands:
210# pre-commit install # set up git hooks
211# pre-commit run --all-files # run on all files manually
212# pre-commit autoupdate # update hook versions
213# git commit -m "feat: add X" # hooks run automatically
214
215
216# ============================================================
217# 7. Complete pyproject.toml — unified configuration
218# ============================================================
219# [project]
220# name = "my-production-app"
221# version = "1.0.0"
222# requires-python = ">=3.12"
223# dependencies = [
224# "fastapi>=0.115.0",
225# "sqlalchemy>=2.0",
226# "pydantic>=2.0",
227# ]
228#
229# [project.optional-dependencies]
230# dev = [
231# "pytest>=8.0",
232# "pytest-cov>=6.0",
233# "pytest-asyncio>=0.24",
234# "ruff>=0.8.0",
235# "mypy>=1.13",
236# "pre-commit>=4.0",
237# ]
238#
239# [tool.pytest.ini_options]
240# testpaths = ["tests"]
241# addopts = "-ra -q --strict-markers --cov=src --cov-report=term-missing"
242# markers = [
243# "slow: marks tests as slow",
244# "integration: marks integration tests",
245# ]
246#
247# [tool.ruff]
248# target-version = "py312"
249# line-length = 88
250#
251# [tool.ruff.lint]
252# select = ["E", "W", "F", "I", "N", "UP", "B", "SIM", "RUF"]
253#
254# [tool.mypy]
255# python_version = "3.12"
256# strict = true
257# warn_return_any = true
258# warn_unused_configs = true
259
260
261# ============================================================
262# 8. Makefile / task runner for common commands
263# ============================================================
264# Makefile:
265# .PHONY: lint format test check all
266#
267# lint:
268# ruff check .
269#
270# format:
271# ruff format .
272# ruff check --fix .
273#
274# typecheck:
275# mypy src/
276#
277# test:
278# pytest --cov=src --cov-report=html
279#
280# check: lint typecheck test
281# @echo "All checks passed!"
282#
283# all: format check

🏋️ Practice Exercise

Exercises:

  1. Set up a Python project from scratch with ruff configured in pyproject.toml. Enable at least 10 rule categories. Write intentionally bad code (unused imports, bare excepts, mutable default args) and run ruff check to see every violation, then fix them with ruff check --fix.

  2. Install and configure pre-commit with hooks for ruff (lint + format), trailing whitespace, large file checks, and debug statement detection. Make a commit with a breakpoint() left in and observe the hook blocking the commit.

  3. Take a 100+ line Python module and run it through pylint, flake8, and ruff. Compare their output: which issues does each tool uniquely catch? Document the overlap and differences.

  4. Configure black and isort (with profile = "black" for compatibility). Format a messy file with both tools and explain why the profile setting matters (hint: trailing commas and import formatting conflicts).

  5. Create a complete pyproject.toml that configures ruff, mypy, pytest, and isort. Add a Makefile with targets for lint, format, typecheck, test, and check (runs all). Verify each target works correctly.

  6. Set up a CI pipeline configuration (GitHub Actions YAML) that runs linting, type checking, and tests on every pull request. Use caching for pip dependencies and fail fast on lint errors before running the full test suite.

⚠️ Common Mistakes

  • Not configuring tools to be compatible with each other. Black and isort can conflict on import formatting — always set profile = "black" in isort config, or use ruff which handles both consistently.

  • Disabling too many linter rules instead of fixing the underlying issues. Linter warnings exist for good reasons. Only disable rules project-wide if you have a documented rationale, and use per-file ignores for legitimate exceptions.

  • Running formatters and linters only in CI but not locally. By the time CI catches an issue, the developer has context-switched. Pre-commit hooks provide instant feedback. IDE integration (VS Code + ruff extension) provides real-time feedback.

  • Not pinning tool versions in CI and pre-commit config. A new ruff or black release can reformat your entire codebase, causing massive diffs. Pin exact versions and update deliberately with pre-commit autoupdate.

  • Treating type checking (mypy) as optional. In production Python, mypy --strict catches entire categories of bugs (None handling, wrong argument types, missing returns) that tests might miss. Enable it early — retrofitting types onto a large codebase is painful.

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Code Quality & Tooling