7.4 KiB
Lint and format with Ruff. It doesn't check types, so add a type checker, and let pre-commit run each Python code analysis tool on every commit.
How to choose:
- Linting, formatting, and import sorting in one tool: Ruff
- Running every check before each commit: pre-commit
- Rules on which of your modules may import which: Import Linter
- Unused code: Vulture
- Functions too complex for people to read: complexipy
- Several analyzers under one command, with ready-made profiles: Prospector
- Black's own formatting, with imports sorted to match: Black and isort
- Checks Ruff can't run: Pylint for deeper inference, Flake8 for your own plugins
- Security issues: Bandit
- Refactoring: Rope from your editor or a script, ast-grep for pattern rewrites
- Type checking Django or Pydantic code: mypy with its plugins, Pyrefly without
- Adding types to a partly typed codebase: ty
- Type checking in VS Code: Pyright, which Pylance runs on
Ruff can replace Flake8, Black, and isort, plus dozens of Flake8 plugins. Its docs suggest you start with a small rule set, add one group at a time, and list them explicitly in select. Ruff is a linter, not a type checker, so run a type checker alongside it.
pre-commit installs and runs the hooks you list in .pre-commit-config.yaml, written in any language. Run pre-commit install first thing after you clone a repo that uses it, so the hooks run on every git commit, and add pre-commit run --all-files as a CI step.
Import Linter checks contracts on the imports between your modules: lint-imports fails when code breaks one. A layers contract lets higher layers import lower ones but not the other way around, and it counts chains of imports through other modules too.
Vulture finds unused code, and running it on your tests too shows you untested code.
complexipy measures cognitive complexity: how hard code is for people to understand. Unlike cyclomatic complexity, it counts nesting. Its docs recommend running it alongside Ruff, whose complexity rule measures something else.
Prospector runs Pylint and other analyzers together, and aims to be useful out of the box, with default profiles so you don't have to sort out which warnings matter. When your project uses Django, Flask, or Celery, it adapts its checks to that framework.
With Black, you give up control over hand-formatting, and in return, code looks the same in every project and diffs stay small. Its style options are deliberately limited. If you sort imports with isort, set the black profile so the two agree.
Pylint infers the actual values in your code instead of trusting its type hints. That makes it slower than other linters, but it finds more in code that isn't fully typed, and it's why Ruff isn't a drop-in replacement for Pylint. Because of that speed, run it in CI or a pre-push hook rather than on every commit.
Flake8 wraps pyflakes, pycodestyle, and McCabe in one command, and its plugins are much of its appeal. Keep it when you depend on a plugin of your own, since Ruff doesn't support custom lint rules.
Bandit finds common security issues in Python code. Scan a whole tree with bandit -r.
Rope is a refactoring library that many IDEs and editors use to rename, extract, and move code, and scripts can run the same refactorings through its library API.
ast-grep matches code by its syntax tree, with patterns you write like ordinary code. Start with a one-off rewrite on the command line: ast-grep run --pattern ... --rewrite ... shows you a diff of the changes. When the same code smell keeps coming back, its docs suggest you write a YAML rule and run all your rules with ast-grep scan.
mypy is designed for gradual typing: by default, it skips functions without annotations, so you can add type hints slowly. Check in one config file, run mypy in CI so new type errors don't land, and aim for --strict. Its plugins teach it libraries the standard type system can't describe.
Pyrefly is a type checker and language server with built-in support for Pydantic and Django, so those work without plugins. It reads an existing mypy or Pyright config, so you can try it alongside your current checker before you switch.
ty is a type checker and language server designed for adoption on partly typed code. Unlike mypy, it checks the bodies of unannotated functions, and it leaves missing-annotation checks to Ruff's ANN rules.
Pyright checks all code by default, annotated or not, which its language server features rely on. In VS Code, its docs recommend Pylance, which includes Pyright. Run it in CI to keep the project type clean, then turn on # pyright: strict file by file.
Pylint, Prospector, Import Linter, and ty all need to see your project's dependencies, so run them from the project's own environment, not a separate one. For more Python code analysis tools, see awesome-static-analysis.