Automatically Checking Code Quality

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.

ToolPurpose
ruff
linter + formatter
Fast Rust-based replacement for flake8/isort/black.
mypy
type checker
Static type verification for Python.
pyright / pylance
type checker
Fast type checker, bundled with VS Code.
black
formatter
Opinionated code formatter.
pylint
linter
Older, slower but very thorough linter.
flake8
linter
Classic PEP 8 + bug checks.
pre-commit
tool
Runs hooks on git commit.
bandit
security
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)