Code Quality
Using linters to catch problems before they become bugs
Concepts
- A linter analyzes code statically (without running it)
and reports violations of style rules, potential bugs, and dangerous patterns
- faster and more exhaustive than a human reviewer for the categories of problems it checks
ruff checkreports violations;ruff formatrewrites files to match the project's style guide automatically- the two tools are complementary, not alternatives
- Different languages need different linters because they have different
syntax and idioms
- ruff handles Python, ESLint handles JavaScript, Markuplint handles HTML
- Linter configuration selects which rule sets are active and records
project-specific exceptions so the choices are explicit and reproducible
pyproject.tomlfor ruff,eslint.config.jsfor ESLint
- Suppressing a linter warning with an inline comment (e.g.,
# noqa: E501) is sometimes the right choice- every suppression should explain why the rule does not apply in that specific case
Why linters?
- A linter is a fast, tireless code reviewer
- Catches style inconsistencies, unused imports, and dangerous patterns automatically
ruff for Python
- Configuration in
pyproject.toml - Running
ruff checkandruff format - Common violations in LLM-generated code and how to fix them
ESLint for JavaScript
- Linting the Alpine.js snippets embedded in htpy output
eslint.config.jsand a taskipy task
Markuplint for HTML
- Validating the HTML that htpy generates
- Configuring markuplint for the patterns this project uses
Making linting part of the workflow
- Running all linters in a single taskipy task
- What to do when a linter disagrees with you and when to override it
Check for Understanding
What is the difference between ruff check and ruff format?
ruff check analyzes the code and reports rule violations without modifying any files;
you see a list of problems and can decide what to do. ruff format rewrites files to
conform to the style guide (consistent indentation, line length, blank lines) without
checking logical correctness. Using both together is typical: format to handle style
automatically, then check for substantive issues.
Name one class of error that a linter can catch that a test suite cannot easily catch.
An unused import: import json at the top of a file that never calls json.anything.
The code works correctly and all tests pass, but the import wastes startup time and
misleads readers into thinking json is used. A linter flags it immediately; a test
suite has no way to notice because nothing is broken. Other examples: unreachable code
after a return, shadowing a built-in name, or a variable defined but never used.
If a linter reports an "unused import", why might suppressing the warning be the wrong fix?
The unused import is usually genuinely unnecessary and should be deleted. Suppressing the warning leaves dead code in the file, which confuses readers who wonder why it is there. The right fix is to remove the import. Suppression is appropriate only in the rare cases where the import has a side effect that is intentional (e.g., registering a plugin) and cannot be expressed differently.
Why do you need three separate linters rather than one?
Each linter understands the syntax, idioms, and failure modes of one language. ruff parses Python ASTs and knows Python-specific rules (undefined names, f-string issues, import order). ESLint parses JavaScript and checks Alpine.js attribute syntax. Markuplint parses HTML and checks for missing attributes, invalid nesting, and accessibility requirements. No single tool understands all three languages well enough to lint them reliably.
Exercises
Audit the LLM's output
Run ruff on all Python code the LLM generated during this tutorial. For each violation reported, categorize it as: (a) style (formatting, naming), (b) correctness (logic error or undefined behavior), or (c) security (dangerous pattern). Fix any correctness or security violations. Note whether the LLM's code had more violations than code you wrote yourself.
Configure a new rule set
Enable ruff's S (flake8-bandit security) rule set in pyproject.toml and run
ruff check again. For each new violation, ask the LLM to explain what the rule
checks for and why it matters. Fix the violations that represent real risks;
suppress (with explanation) any that are false positives in this application's
context.
Add a pre-commit check
Modify the check task in pyproject.toml so that it runs ruff, ESLint, and
Markuplint in sequence and fails if any of them report errors. Add a note to the
README explaining that uv run task check should be run before committing changes.
Override a rule deliberately
Find a linter rule that flags something you have deliberately chosen to do (for example, a line length limit that would break a long URL, or a naming convention for a variable that mirrors a mathematical symbol). Suppress the warning with an inline comment and write the comment so that a future reader understands exactly why the override exists.