Code Quality & Tooling
📖 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-commitframework — 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
1# ============================================================2# 1. Ruff — Modern all-in-one linter + formatter3# ============================================================4# Install: pip install ruff (or uv add ruff)56# pyproject.toml configuration:7# [tool.ruff]8# target-version = "py312"9# line-length = 8810# src = ["src"]11#12# [tool.ruff.lint]13# select = [14# "E", # pycodestyle errors15# "W", # pycodestyle warnings16# "F", # pyflakes17# "I", # isort18# "N", # pep8-naming19# "UP", # pyupgrade20# "B", # flake8-bugbear21# "SIM", # flake8-simplify22# "RUF", # ruff-specific rules23# "S", # flake8-bandit (security)24# "C4", # flake8-comprehensions25# "DTZ", # flake8-datetimez26# "T20", # flake8-print (no print in production)27# "PT", # flake8-pytest-style28# ]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 tests36#37# [tool.ruff.format]38# quote-style = "double"39# indent-style = "space"4041# Commands:42# ruff check . # lint43# ruff check --fix . # lint with auto-fix44# ruff format . # format (like black)45# ruff format --check . # check formatting without changing464748# ============================================================49# 2. Black — Opinionated code formatter50# ============================================================51# Install: pip install black5253# pyproject.toml:54# [tool.black]55# line-length = 8856# target-version = ["py312"]57# include = '\.pyi?$'5859# Before black:60def messy_function( x,y ,z ):61 return {"key1":x,"key2":y ,62 "key3" :z}6364result=messy_function(1,2,65 3 )6667# After black formats it:68def messy_function(x, y, z):69 return {"key1": x, "key2": y, "key3": z}707172result = messy_function(1, 2, 3)7374# Commands:75# black . # format all files76# black --check . # check without modifying77# black --diff . # show what would change787980# ============================================================81# 3. isort — Import sorting82# ============================================================83# Install: pip install isort8485# pyproject.toml:86# [tool.isort]87# profile = "black" # compatible with black formatting88# src_paths = ["src"]89# known_first_party = ["myproject"]90# sections = [91# "FUTURE",92# "STDLIB",93# "THIRDPARTY",94# "FIRSTPARTY",95# "LOCALFOLDER",96# ]9798# Before isort:99# import os100# from myproject.utils import helper101# import sys102# import requests103# from pathlib import Path104# from collections import defaultdict105# import json106107# After isort:108import json109import os110import sys111from collections import defaultdict112from pathlib import Path113114import requests115116from myproject.utils import helper117118119# ============================================================120# 4. pylint — Comprehensive static analysis121# ============================================================122# Install: pip install pylint123124# pyproject.toml:125# [tool.pylint.messages_control]126# disable = [127# "C0114", # missing-module-docstring128# "C0115", # missing-class-docstring129# "R0903", # too-few-public-methods130# ]131#132# [tool.pylint.format]133# max-line-length = 88134135# pylint catches things other linters miss:136class DatabaseConnection:137 def __init__(self, host, port):138 self.host = host139 self.port = port140 self._connection = None141142 def connect(self):143 # pylint would warn: attribute '_connection' defined outside __init__144 # if we added self._new_attr = "oops" here145 self._connection = f"connected to {self.host}:{self.port}"146 return self._connection147148 def query(self, sql):149 if not self._connection:150 raise RuntimeError("Not connected")151 return f"Executing: {sql}"152153154# ============================================================155# 5. flake8 — Lightweight PEP 8 + logic checker156# ============================================================157# Install: pip install flake8158159# .flake8 or setup.cfg (flake8 doesn't support pyproject.toml natively):160# [flake8]161# max-line-length = 88162# extend-ignore = E203, W503163# per-file-ignores =164# tests/*.py: S101165# max-complexity = 10166167# Popular flake8 plugins:168# pip install flake8-bugbear # additional bug checks169# pip install flake8-comprehensions # better list/dict/set comprehensions170# pip install flake8-bandit # security checks (wraps bandit)171172# Commands:173# flake8 . # check all files174# flake8 --statistics # summary of violations175# flake8 --max-complexity 10 # enforce cyclomatic complexity176177178# ============================================================179# 6. Pre-commit hooks — Automate checks before every commit180# ============================================================181# Install: pip install pre-commit182183# .pre-commit-config.yaml:184# repos:185# - repo: https://github.com/astral-sh/ruff-pre-commit186# rev: v0.8.0187# hooks:188# - id: ruff189# args: [--fix]190# - id: ruff-format191#192# - repo: https://github.com/pre-commit/pre-commit-hooks193# rev: v5.0.0194# hooks:195# - id: trailing-whitespace196# - id: end-of-file-fixer197# - id: check-yaml198# - id: check-added-large-files199# args: [--maxkb=500]200# - id: check-merge-conflict201# - id: debug-statements # catches breakpoint() and pdb202#203# - repo: https://github.com/pre-commit/mirrors-mypy204# rev: v1.13.0205# hooks:206# - id: mypy207# additional_dependencies: [types-requests]208209# Commands:210# pre-commit install # set up git hooks211# pre-commit run --all-files # run on all files manually212# pre-commit autoupdate # update hook versions213# git commit -m "feat: add X" # hooks run automatically214215216# ============================================================217# 7. Complete pyproject.toml — unified configuration218# ============================================================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 = 88250#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 = true257# warn_return_any = true258# warn_unused_configs = true259260261# ============================================================262# 8. Makefile / task runner for common commands263# ============================================================264# Makefile:265# .PHONY: lint format test check all266#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=html279#280# check: lint typecheck test281# @echo "All checks passed!"282#283# all: format check
🏋️ Practice Exercise
Exercises:
Set up a Python project from scratch with
ruffconfigured inpyproject.toml. Enable at least 10 rule categories. Write intentionally bad code (unused imports, bare excepts, mutable default args) and runruff checkto see every violation, then fix them withruff check --fix.Install and configure
pre-commitwith hooks for ruff (lint + format), trailing whitespace, large file checks, and debug statement detection. Make a commit with abreakpoint()left in and observe the hook blocking the commit.Take a 100+ line Python module and run it through
pylint,flake8, andruff. Compare their output: which issues does each tool uniquely catch? Document the overlap and differences.Configure
blackandisort(withprofile = "black"for compatibility). Format a messy file with both tools and explain why theprofilesetting matters (hint: trailing commas and import formatting conflicts).Create a complete
pyproject.tomlthat configures ruff, mypy, pytest, and isort. Add aMakefilewith targets forlint,format,typecheck,test, andcheck(runs all). Verify each target works correctly.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 --strictcatches 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