How to Check Python Code Style Locally with pycodestyle, flake8 and ruff

An online checker is handy for a snippet. For a real project you want the same checks on your own machine, on every file, with settings that live in the repository and run the same way for everyone. This guide sets that up with pycodestyle, flake8 or ruff, using a tiny example project.
Step 1: a virtual environment
Install development tools per project, not globally, so each project pins its own versions. The standard library's venv module creates an isolated environment:
python3 -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
python -m pip install pycodestyle flake8 ruff
Using python -m pip rather than a bare pip guarantees that packages go into the interpreter you are actually using. You only need one of the three tools; they are installed together here to compare them.
Step 2: run pycodestyle
Our example project has one module, src/shop/cart.py, with several deliberate problems:
import os, sys
def total(items):
return sum(item.price for item in items if item.price!=None)
Running pycodestyle on the folder gives one line per problem, in the format file:line:column: code message:
$ pycodestyle src/
src/shop/cart.py:1:10: E401 multiple imports on one line
src/shop/cart.py:2:1: E302 expected 2 blank lines, found 0
src/shop/cart.py:3:58: E225 missing whitespace around operator
src/shop/cart.py:3:58: E711 comparison to None should be 'if cond is not None:'
The command exits with status 1 when it finds anything, which is what CI systems use to fail a build. Useful options include --max-line-length=88, --statistics (count each code), --show-source and --show-pep8 (print the offending line and the relevant passage of PEP 8). Each code is explained in our reference to pycodestyle codes.
Notice what pycodestyle did not report: os and sys are imported but never used. That is not a style issue, so it is out of pycodestyle's scope.
Step 3: flake8, for style plus errors
flake8 runs pycodestyle together with pyflakes, which finds likely bugs such as unused imports and undefined names, and the mccabe complexity checker (disabled until you set max-complexity). On the same file, flake8 src/ reports the four pycodestyle codes plus F401 'os' imported but unused and the same for sys. Its configuration goes in setup.cfg, tox.ini or a .flake8 file:
[flake8]
max-line-length = 88
extend-ignore = E203
exclude = .venv,build
flake8 is extended through plugins installed with pip, for example pep8-naming for naming conventions; a plugin that pycodestyle's own documentation recommends.
Step 4: ruff, configured in pyproject.toml
ruff reimplements the checks of pycodestyle, pyflakes and many flake8 plugins in a single tool, and adds a formatter. Its rule selection has changed between releases, so set it explicitly rather than relying on the default. A minimal configuration in pyproject.toml:
[tool.ruff]
line-length = 88
src = ["src"]
[tool.ruff.lint]
select = ["E", "W", "F", "I"]
This enables pycodestyle errors and warnings (E, W), pyflakes (F) and import sorting (I). The src setting tells ruff where your own packages live, so it can tell them from third-party imports. With it, ruff check src/ reported:
src/shop/cart.py:1:1: E401 [*] Multiple imports on one line
src/shop/cart.py:1:1: I001 [*] Import block is un-sorted or un-formatted
src/shop/cart.py:1:8: F401 [*] `os` imported but unused
src/shop/cart.py:1:12: F401 [*] `sys` imported but unused
src/shop/cart.py:3:60: E711 Comparison to `None` should be `cond is not None`
Found 5 errors.
[*] 4 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
The asterisks mark problems ruff can fix itself. The note about a "hidden fix" concerns E711: changing != None to is not None could change behaviour if a class defines an unusual __eq__, so ruff classes that fix as unsafe and leaves it to you. Our ruff guide covers rule selection in more detail.
Step 5: fix automatically
Two different kinds of tools fix code:
- Linters with fixes change specific problems:
ruff check --fix src/removed the unused imports here. - Formatters rewrite layout as a whole:
ruff format src/orblack src/added the two blank lines and the spaces around!=.
After both, only the E711 issue remained, which needed a human decision. Run fixers on a clean working tree, so you can review the diff before committing. ruff format --diff and black --diff show changes without writing them.
Choosing what to check
All three tools accept files and directories, and skip common folders such as .git by default. Exclude generated code and virtual environments explicitly (--exclude for pycodestyle and flake8, extend-exclude in ruff's configuration), otherwise you will lint thousands of files that are not yours. To focus on one kind of problem, select a code family: pycodestyle --select=E3 src/ shows only blank-line issues, and ruff check --select F401 . only unused imports. That is a good way to clean up an old project in small, reviewable steps rather than one enormous change.
Step 6: make it automatic
Checks that depend on someone remembering to run them do not stay run. Three places make them automatic:
- The editor. Most editors have extensions that run ruff, flake8 or a formatter on save and underline problems as you type.
- A pre-commit hook, which runs the checks on the files you are about to commit; the pre-commit framework is the usual way to manage them.
- Continuous integration, running the same command as everyone else, for example
ruff check . && ruff format --check .. The--checkflag makes the formatter fail instead of rewriting files.
A fresh laptop is a good moment to set all of this up properly, whether it comes with a new project or with moving to a new city for a developer job, when a dozen other things compete for your first weeks.
Which one should you use?
For an existing project, use whatever its configuration already names. For a new one, the choice is mostly about scope: pycodestyle checks style only and has no dependencies; flake8 adds error detection and a large plugin ecosystem; ruff covers most of both, plus formatting and import sorting, in one fast tool. For a quick look at a snippet without installing anything, the checker on our home page runs a documented subset of pycodestyle's checks in your browser.
Sources
- pycodestyle documentation, "Introduction" (usage, options, configuration).
- flake8 documentation, "Configuring Flake8".
- Python documentation,
venv. - ruff documentation, "Configuring Ruff" and "Fix safety". Output above produced with pycodestyle 2.15 and ruff 0.16 on Python 3.13.
Linters & formatterspycodestyle error codes
What E225, E302, E501 or W291 actually mean and how to fix each one: a family-by-family reference to the codes used by pycodestyle, flake8 and ruff.
Linters & formattersRuff: linter and formatter
One tool for linting, import sorting and formatting: how ruff's rule codes work, how to choose them, and what its fixes will and will not change.
Linters & formattersPre-commit hooks
Catch trailing whitespace, unsorted imports and formatting before they reach a commit: git hooks, the pre-commit framework and a configuration to start from.