Code Quality

Using linters to catch problems before they become bugs

Concepts

Why linters?

ruff for Python

ESLint for JavaScript

Markuplint for HTML

Making linting part of the workflow

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.