Linting & Formatting Best Practices
A low-friction lint and format setup that catches real bugs without slowing developers down.
How to Use This List
- Implement in order: formatter, linter, pre-commit, CI
- Tune per-directory ignores only with documented reasons
- Review rule set quarterly when upgrading ruff
A - Tooling Choices
- Use ruff for lint and format. One tool, one config file, sub-second runs.
- Configure in
pyproject.toml. No scattered.flake8,setup.cfg, or.isort.cfg. - Set
target-version = "py314". Rules match your Python version. - Add mypy or pyright for type checking. Ruff catches style; types catch logic errors.
B - Rule Selection
- Start with E, F, I, UP, B, SIM. Add security (S) for web apps and CLIs.
- Ignore E501. Let the formatter handle line length.
- Per-file ignores for tests and migrations.
tests/**allows assert;migrations/**excluded. - No
select = ["ALL"]on day one. Add rules incrementally to avoid alert fatigue.
C - Developer Workflow
- Format on save in the editor. Commit
.vscode/settings.jsonfor the team. - Install pre-commit hooks.
pre-commit installin README bootstrap steps. - Run
ruff check --fixbefore pushing. Catches what pre-commit missed. - Never debate formatting in code review. The formatter decides.
D - CI Enforcement
-
ruff check .andruff format --check .in CI. Both are required checks. - Pin ruff version in lockfile and pre-commit
rev. Local and CI stay in sync. - mypy/pyright as a required PR check. Not
continue-on-error. - Cache lint artifacts.
.ruff_cache,.mypy_cachekeyed on lockfile.
E - Team Culture
- Every
noqaandtype: ignoreneeds a comment. Explains why the rule is suppressed. - Fix lint violations in the file you touch. Boy-scout rule reduces backlog.
- Schedule lint backlog cleanup sprints. Don't let ignores accumulate forever.
- Upgrade ruff quarterly. New rules often catch real bugs.
FAQs
What is the minimum viable lint setup?
ruff check + ruff format + CI. Add mypy when the codebase has type hints.
How do I migrate from flake8?
Map flake8 plugins to ruff rule codes. Enable equivalent sets. Remove flake8 from CI.
Should I lint auto-generated code?
Exclude it. Add paths to exclude in ruff config.
How strict should mypy be?
check_untyped_defs first. Move to strict module by module.
Can I skip pre-commit?
CI must still run the same checks. pre-commit saves CI round-trips.
How do I handle legacy lint debt?
Enable rules as warnings first. Fix per-module. Flip to errors when clean.
Black or ruff format?
ruff format for new projects. Equivalent output, faster execution.
Should notebooks be linted?
Yes, with ruff format notebook.ipynb. Exclude output cells if noisy.
How do I lint a monorepo?
One pyproject.toml at root with src paths, or per-package configs with extend.
What about security linting?
Enable S (bandit) rules for any code handling user input, secrets, or SQL.
Related
- Ruff Setup - getting started
- Essential Lint Rules - rule recommendations
- pre-commit Hooks - local automation
- Linting in CI/CD - pipeline gates
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.