Ruff for linting and formatting, a type checker alongside it, and pre-commit to run them, with per-tool guidance sourced from each project's own docs. Co-Authored-By: Claude <noreply@anthropic.com>
7.0 KiB
Run Ruff for Python code analysis: it lints and formats. Add a type checker, since Ruff doesn't check types, and pre-commit to run Ruff on every commit.
How to choose:
- Rules on which modules may import which: Import Linter
- Dead code: Vulture, or repowise to index the repo for your agent
- Deeply nested functions that are hard to read: complexipy
- Several analysis tools behind one command: Prospector
- Checks before every commit: pre-commit
- Linting, formatting, and import sorting: Ruff
- Formatting without Ruff: Black, with isort on its black profile
- Deeper inference and checks of your own: Pylint
- Flake8 plugins Ruff doesn't have: Flake8
- Security issues: Bandit
- Refactoring: Rope
- Type checking: mypy, or Pyright, ty, or Pyrefly to also check unannotated code
- Type hints from the types your code sees at runtime: MonkeyType
Ruff lints and formats in one tool, and its FAQ lists what it can replace: Flake8 and dozens of its plugins, Black, and isort. To sort imports and format, run the linter, then the formatter: ruff check --select I --fix, then ruff format. When you turn on a new rule in an existing codebase, --add-noqa marks the current violations, so the rule only applies to new code.
Pick one formatter and stay with it. Ruff's formatter is designed as a drop-in replacement for Black, but it's not meant to be used interchangeably with Black over time. If you pick Black, set isort's black profile in a config file at the root of your repo, so it applies however isort runs. To adopt Black, reformat everything in one commit and list that commit in .git-blame-ignore-revs, so git blame skips it.
Ruff can be a drop-in replacement for Flake8 when your code is formatted with Black and uses few or no Flake8 plugins. Keep Flake8 when you depend on a plugin Ruff doesn't cover. In pre-commit, install the plugin through additional_dependencies.
Pylint doesn't trust your type hints. It infers the actual values instead, which makes it slower but finds more issues in code that isn't fully typed. You can also write plugins for checks of your own. On a legacy project, start with --errors-only, then turn more messages on over time. Because of its speed, Pylint's docs suggest running it in CI or a pre-push hook, not on every commit.
Bandit finds common security issues by walking each file's syntax tree. On an existing project, save a baseline to ignore the findings you've judged to be non-issues. When you skip a line, write # nosec B602 instead of a bare # nosec, so a new issue on that line still shows up.
Ruff is a linter, not a type checker, so add one. mypy is designed for gradual typing, and by default it skips functions without annotations. On an existing codebase, its docs suggest starting with part of the code, running it in CI early, turning on check_untyped_defs as soon as you can, and aiming for mypy --strict.
Pyright, ty, and Pyrefly each come with a language server for your editor, and they check unannotated code too. Pyright checks all code regardless of annotations. Commit its config, run it in CI, and turn on strict mode file by file with # pyright: strict.
ty is designed for adoption, with support for partially typed code. Add it as a dev dependency, so everyone runs the same version. Pick Pyrefly when your code uses Pydantic, Django, or attrs and you don't want the overhead of plugins. To switch to it, pyrefly init migrates your existing type checker config, and pyrefly suppress marks the current errors as ignored, so you start from a clean check.
Import Linter checks contracts on your imports, such as layers: higher layers may import lower ones, not the other way around. Vulture finds unused code. For its false positives, it recommends a whitelist over noqa comments. After you delete dead code, run it again, since it may find more. complexipy measures how hard code is for people to understand, and its docs say to run it alongside Ruff: Ruff catches wide functions, complexipy catches deep ones. On a large existing codebase, take a snapshot first, so only new complexity fails.
Prospector wraps several analysis tools and aims to be useful out of the box. repowise indexes your repo for coding agents and developers, with dead code and git history in the same index. It's licensed under the AGPL, or a commercial license.
Rope is a refactoring library, and its wiki suggests starting with its language server plugin before the native editor integrations. MonkeyType records the types your code sees at runtime and writes annotations from them, which its docs call an informative first draft for you to check and fix. mypy's docs suggest collecting types from test runs this way.
pre-commit runs your hooks before every commit. Run pre-commit install after every clone, pre-commit run --all-files when you add a hook, and the same command in CI. With Ruff's hooks, put the lint hook with --fix before the formatter, since its fixes can leave code that needs reformatting.
The checkers here do static code analysis: they check your code without running it. MonkeyType is the exception, since it records types while your code runs. Run Ruff in your editor and on every commit, and Pylint and your type checker in CI.