Automated code-quality checks run on every change and catch issues long before code review. They cover style (PEP 8 violations, inconsistent whitespace), likely bugs (unused variables, shadowed imports), type mismatches, and security smells. Setting them up once and wiring them into your editor and CI pays off on every commit afterwards.
The three most useful tool families are linters, formatters, and type checkers. Linters (ruff, flake8, pylint) warn about rule violations and dangerous patterns. Formatters (black, ruff format) apply a consistent style without argument. Type checkers (mypy, pyright) verify that function calls match their declared signatures.
The dominant choice in 2026 is ruff plus mypy. ruff combines lint and format into one fast Rust-based tool that replaces flake8, isort, and most of pylint. mypy remains the de-facto type checker. Configure both in pyproject.toml; run them in pre-commit hooks and CI.
The trick with these tools is not to let them become background noise. Fix the warnings they produce; suppress a rule only with a written reason (# noqa: E501 with a comment). If a rule is wrong for your project, disable it explicitly. A clean report is worth aiming for; a huge report of ignored warnings is worse than none.
Lint, format, type-check
Run ruff check . for lint warnings. Run ruff format . (or black .) to apply formatting. Run mypy src/ for type checks. Each step takes a few seconds on a small project; the feedback is almost immediate.
Put them all behind pre-commit: one pre-commit install and every git commit runs the checks. Failures block the commit until fixed. That prevents bad code from ever entering the history.
Type hints as first-class quality
Add : Type annotations to parameters and return values. Use Iterable, Sequence, Mapping over list/dict in signatures when you only need to iterate or look up. Add TypedDict, NamedTuple, or dataclass for structured records.
Turn on strict = true in [tool.mypy] once the codebase is mostly annotated. It catches dozens of bugs that hide in dynamically-typed code (missing returns, wrong comparisons, None slipping through).
Quality tools and where they fit.
| Tool | Purpose |
|---|---|
rufflinter + formatter | Fast Rust-based replacement for flake8/isort/black. |
mypytype checker | Static type verification for Python. |
pyright / pylancetype checker | Fast type checker, bundled with VS Code. |
blackformatter | Opinionated code formatter. |
pylintlinter | Older, slower but very thorough linter. |
flake8linter | Classic PEP 8 + bug checks. |
pre-committool | Runs hooks on git commit. |
banditsecurity | Security linter for common Python issues. |
Automatically Checking Code Quality code example
The script uses Python's built-in ast module to simulate a mini-linter that flags bare except: clauses — a classic anti-pattern.
# Lesson: Automatically Checking Code Quality
import ast
import textwrap
SOURCE = textwrap.dedent('''
def bad_one():
try:
do_something()
except: # bare except — hides bugs
pass
def ok():
try:
do_something()
except ValueError:
pass
def shadowed():
list = [1, 2] # shadows built-in
return list
''').strip()
class MiniLinter(ast.NodeVisitor):
def __init__(self):
self.warnings: list[tuple[int, str]] = []
def visit_ExceptHandler(self, node: ast.ExceptHandler):
if node.type is None:
self.warnings.append((node.lineno, "bare except (W001)"))
self.generic_visit(node)
def visit_Assign(self, node: ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in {"list", "dict", "str", "set"}:
self.warnings.append((node.lineno, f"shadows built-in '{target.id}' (W002)"))
self.generic_visit(node)
tree = ast.parse(SOURCE)
linter = MiniLinter()
linter.visit(tree)
print("source lines:", len(SOURCE.splitlines()))
print("warnings:")
for line_no, msg in linter.warnings:
print(f" L{line_no:2d}: {msg}")
How the mini-linter works:
1) `ast.parse` turns the source into a tree without running it.
2) `NodeVisitor.visit_ExceptHandler` fires for every `except` clause.
3) `node.type is None` detects bare `except:` — the classic bug-hider.
4) Shadowing built-ins is flagged similarly via `ast.Assign` targets.
Add a third rule: flag functions with no type annotations.
import ast
src = """
def add(a, b):
return a + b
def typed(a: int, b: int) -> int:
return a + b
"""
tree = ast.parse(src)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
if not node.returns or any(a.annotation is None for a in node.args.args):
print(f"W003 missing type hints: {node.name} (line {node.lineno})")
Confirm the linter on a known input.
import ast
src = """
try:
pass
except:
pass
"""
tree = ast.parse(src)
bare = [n for n in ast.walk(tree) if isinstance(n, ast.ExceptHandler) and n.type is None]
assert len(bare) == 1
Running prints:
source lines: 13
warnings:
L 5: bare except (W001)
L13: shadows built-in 'list' (W002)