4.1. Linting
What is software linting?
Linting is the process of using a static code analysis tool—a "linter"—to check source code for programmatic errors, bugs, stylistic inconsistencies, and other potential issues. It acts as an automated code reviewer, flagging problems without executing the program.
Why are linters essential for development?
Integrating linters into a development workflow provides significant benefits:
- Enforce Code Quality: Linters automatically enforce coding standards (like PEP 8 for Python), ensuring consistency and maintainability, which is critical for collaborative projects.
- Enhance Readability: By standardizing style, linters make code easier to read and understand for all team members, streamlining code reviews and onboarding.
- Prevent Bugs: They detect common errors, such as syntax mistakes, undefined variables, or problematic patterns, catching bugs before they reach production.
- Accelerate Learning: For developers new to a language or a team, linters provide immediate feedback, helping them learn and adopt best practices quickly.
Which linting tool is recommended?
Ruff is the recommended linter for modern Python projects. It is written in Rust and is exceptionally fast, often hundreds of times faster than other linters like Pylint. Its speed allows for real-time feedback in your editor without impacting performance.
Key advantages of Ruff include:
- Speed: Get instant feedback as you write code.
- All-in-One: It combines the functionality of multiple tools (e.g., pylint, pyflakes, isort) into a single, cohesive package.
- Auto-Fixing: Ruff can automatically fix many of the issues it detects, saving you time and effort.
- VS Code Extension: The official Ruff VS Code extension integrates these features directly into your editor.
# Install Ruff into your "dev" dependency group
uv add --group dev ruff
# Run Ruff to lint your codebase
uv run ruff check src/ tests/
To keep your repository clean, remember to add the .ruff_cache/ directory to your .gitignore file.
How do you configure a linter?
Linter configurations are typically placed in the pyproject.toml file. This allows you to define project-wide rules, customize behavior, and ensure every developer uses the same settings.
Here is the configuration used by the MLOps Python Package, abridged to show its shape:
[tool.ruff]
# define the default line length
line-length = 120
# define the default python version
target-version = "py314"
[tool.ruff.lint]
# an explicit, reviewed selection instead of Ruff's evolving defaults
select = [
"B", # flake8-bugbear
"D", # pydocstyle (documented public API)
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort (import sorting)
"N", # pep8-naming
"PTH", # flake8-use-pathlib
"RUF", # ruff-specific rules
"S", # flake8-bandit (security; replaces bandit)
"SIM", # flake8-simplify
"T20", # flake8-print
"UP", # pyupgrade
"W", # pycodestyle warnings
]
ignore = [
"E501", # line length handled by the formatter
]
[tool.ruff.lint.pydocstyle]
# set the expected docstring style
convention = "google"
[tool.ruff.lint.per-file-ignores]
# exceptions for docstrings and asserts in tests
"tests/**" = ["D100", "D103", "S101"]
If you need to ignore a specific rule for a single line, you can use an inline noqa (no quality assurance) comment:
# Ignore the "unused import" error (F401) for this specific line
from project.module import specific_import # noqa: F401
Why should you write an explicit rule selection?
Ruff ships with a default rule set, and that default set is not stable across releases. Ruff 0.16.0 (released 2026-07-23) expanded it from 59 rules to 413, pulling in whole families that had previously been opt-in. You can verify the current number yourself:
# Print the settings Ruff would use with no configuration at all
uv run ruff check --show-settings --isolated .
The consequence depends entirely on how your project is configured:
- With an explicit
selectlist (the reference repositories), nothing changed on upgrade. You opted into thirty-one named families, Ruff enforces exactly those, and a release that broadens the defaults is invisible to you. New rules arrive when you decide to add a family, not when a dependency resolves. - Without a
selectlist, the upgrade silently multiplied your enforced rules by seven. Auv syncthat pulled Ruff 0.16 could turn a green repository red with hundreds of violations in code nobody had touched, and the diff that "caused" it would be a lockfile bump.
This is the practical argument for writing select explicitly, even if your initial selection matches today's defaults: the list becomes a reviewed decision recorded in your repository, and upgrading the linter stops being a source of surprise failures.
Why does your Ruff version floor matter?
Linting and formatting must agree across every machine that runs them: your editor, your teammates' checkouts, your git hooks, and CI. Ruff 0.16 changed formatter behavior too (it now formats Python inside Markdown by default, see 4.5. Formatting), which means an older Ruff disagrees with a repository formatted by 0.16. A contributor whose environment resolved 0.15 would see ruff format --check fail on files they never opened.
The fix is to raise the dependency floor whenever you adopt a behavior change, and to say why in a comment:
[dependency-groups]
dev = [
# Ruff 0.16 formats Python inside Markdown and rewrote the default rule set:
# an older Ruff would disagree with this repository's formatting, so floor it.
"ruff>=0.16.2",
]
Pair that floor with a committed uv.lock (see 1.3. uv (project)) so everyone resolves the same version, and with uv lock --check in your check:format task so a stale lockfile fails the gate.
How does linting differ from formatting?
While often used together, linting and formatting have distinct purposes: - Linting analyzes code for correctness and adherence to quality standards. It catches potential bugs and logical errors. - Formatting focuses purely on style. It automatically rewrites code to enforce consistent layout, spacing, and line breaks, without changing its logic.
Tools like Ruff can perform both linting and formatting, providing a comprehensive solution for code quality and style consistency.
What are the best practices for linting?
- Integrate Linting into Your Editor: Configure your IDE or code editor to run the linter automatically, providing immediate feedback as you type.
- Automate with Pre-Commit Hooks: Run the linter on staged files before they are committed. This practice catches issues early and keeps the main branch clean.
- Enforce Linting in CI/CD: Add a linting step to your Continuous Integration (CI) pipeline to prevent code that violates standards from being merged.
- Start with Sensible Defaults: Begin with the linter's default rule set and customize it over time by adding or ignoring rules that fit your project's specific needs.
- Use Linting in Code Reviews: Make passing the linter a prerequisite for code review. This allows reviewers to focus on the logic and architecture instead of style debates.
- Keep Rules Consistent: Ensure the entire team understands and adheres to the linting configuration to maintain a uniform codebase.