flake8
flake8 is a command that runs three other tools over your Python files and merges their output into one list of problems. pyflakes finds real errors such as unused imports, undefined names, and shadowed variables. pycodestyle checks formatting against PEP 8, which is where the E and W codes come from. mccabe measures cyclomatic complexity if you ask it to. On top of that flake8 adds the parts people actually think of as flake8: the noqa comment system for suppressing a single line, per-file ignore rules, config file discovery, parallel execution across files, and a plugin interface that hundreds of third-party checkers register against. It does not reformat anything and it does not do type checking. It reads your code, prints filename, line, column, code, and message, and exits non-zero if it found anything.
flake8 still works, is still maintained, and if your project already runs it there is no emergency. For anything new, Ruff covers the same rules with autofix and pyproject.toml config, and the main reason left to stay on flake8 is a plugin that has no Ruff equivalent.
Use it if
- You have an existing project already wired to flake8, with a setup.cfg full of tuned ignores and a plugin set your team relies on, and switching linters is not worth the churn this quarter
- You depend on a plugin with no equivalent elsewhere: flake8-bandit, flake8-django, flake8-pytest-style variants, or an internal checker your company wrote against the flake8.extension entry point
- You want a small, predictable tool with a rule set that has barely moved in years, so a CI pipeline that passes today still passes after a patch bump
- You need to run on Python 3.9, which flake8 still supports
- You want error detection separate from formatting, so black or another formatter owns layout and flake8 only reports genuine mistakes
- You are starting a new project in 2026. Ruff reimplements the pyflakes and pycodestyle rule sets plus most popular flake8 plugins in one Rust binary, runs far faster on a large repo, and configures from pyproject.toml, which is where every other Python tool now lives
- You want configuration in pyproject.toml. flake8 only reads setup.cfg, tox.ini, or .flake8, and the maintainers have declined to add pyproject support, so you keep a second config file purely for this one tool
- You expect it to fix anything. There is no autofix mode at all: flake8 reports, you edit. Even the trivially mechanical fixes such as unused imports need a separate tool
- You have a tight dependency graph. flake8 7.3.0 pins pycodestyle >=2.14.0,<2.15.0, pyflakes >=3.4.0,<3.5.0, and mccabe >=0.7.0,<0.8.0, so a plugin that pins a different pycodestyle range makes the resolver unhappy and upgrading flake8 can force you to upgrade or drop plugins
- Speed matters. It starts a Python process, imports every installed plugin, and re-parses each file per checker; on a large monorepo that is the slow step in pre-commit and CI, and --jobs only helps so much
- You want linting and formatting from one tool. flake8 has no opinion on layout, so you end up running flake8 plus black plus isort plus mypy, each with its own config and its own pre-commit hook
Setup reality
pip install flake8 pulls pycodestyle, pyflakes, and mccabe at pinned version ranges, and running flake8 with no arguments lints the current directory. Then you hit the defaults. max-line-length is 79, which nothing in modern Python actually uses, and the default ignore list is E121, E123, E126, E226, E24, E704, W503, and W504, so the tool is already lying slightly about being pure PEP 8. If you use black, you have to reconcile them by hand: set max-line-length to 88 and add E203 to extend-ignore, because black's slice spacing trips pycodestyle. Config discovery is the other surprise. flake8 looks for setup.cfg, tox.ini, or .flake8 in the directory tree and deliberately does not read pyproject.toml, so a project that has consolidated everything else into pyproject keeps one extra file just for this. Use extend-ignore rather than ignore unless you mean to throw away the defaults, and the same for extend-select. Plugins install as ordinary packages and register through the flake8.extension entry point, which means they are active the moment they are installed, in every project sharing that environment; --require-plugins is how you make a missing one an error rather than a silent gap. Finally, note that flake8 caches nothing between runs and --jobs auto uses multiprocessing, which is unavailable on some Windows and container setups and quietly falls back to serial.
Patterns
Lint a project from the command lineinstall-and-run
pip install flake8
flake8 # current directory
flake8 src/ tests/ # specific paths
flake8 --count src/ # print a total at the end
# src/app.py:12:1: F401 'os' imported but unused
# src/app.py:44:80: E501 line too long (92 > 79 characters)Exit code is 1 when anything was reported and 0 when clean, which is all CI needs. The default line length of 79 is why a fresh run on a normal codebase floods with E501 before you have configured anything.
Put settings in setup.cfg, tox.ini, or .flake8config-file
# setup.cfg
[flake8]
max-line-length = 88
extend-ignore = E203, E704
extend-exclude = .venv,build,migrations
max-complexity = 10
per-file-ignores =
__init__.py:F401
tests/*:S101flake8 does not read pyproject.toml, so this file exists purely for it. Use extend-ignore, not ignore: setting ignore replaces the built-in default list (E121, E123, E126, E226, E24, E704, W503, W504) and turns those checks back on.
Stop flake8 arguing with blackblack-compatibility
[flake8]
max-line-length = 88
extend-ignore = E203, E501, W503
# E203: whitespace before ':' (black formats slices this way)
# W503: line break before binary operator (black prefers it)
# E501: only if you let black own line length entirelyblack and pycodestyle genuinely disagree about slice spacing and operator line breaks, so this is not optional if you run both. Dropping E501 is a choice: black will not split a long string or comment, so those lines stay long and unflagged.
Silence one line or one fileinline-suppression
import os # noqa
import sys # noqa: F401
from x import y, z # noqa: F401,E501
# first line of a generated file, at column 0:
# flake8: noqaAlways give the code. A bare # noqa hides everything on that line, including a real error introduced later. A file-level # flake8: noqa must start the line; put it after code and flake8 prints a warning and ignores it. Codes on the file-level form are not supported.
Choose which checks runselect-and-ignore
flake8 --select=E9,F63,F7,F82 . # syntax and undefined names only
flake8 --extend-select=C901 . # add complexity on top of defaults
flake8 --extend-ignore=E501,W291 . # keep defaults, drop two more
flake8 --disable-noqa . # audit what noqa is hiding--select replaces the entire active set, --extend-select adds to it. The four-code select above is the common CI smoke test: it catches syntax errors and undefined names without any style opinions. --disable-noqa is worth running once a quarter.
Relax rules for specific pathsper-file-ignores
[flake8]
per-file-ignores =
__init__.py:F401,F403
tests/**:D103
scripts/one_off_*.py:T201Patterns match against the path as given on the command line, so a rule written for tests/** does not fire when someone runs flake8 from inside the tests directory. This replaces the old trick of littering __init__.py with noqa comments.
Run it through pre-commitpre-commit-hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pycqa/flake8
rev: 7.3.0
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear==24.12.12pre-commit builds an isolated environment, so plugins must go in additional_dependencies or they are simply not there and their codes silently never fire. Pin the plugin version too, otherwise a new plugin release breaks CI on a day you changed nothing.
Add and enforce pluginsplugins
pip install flake8-bugbear flake8-comprehensions
flake8 --version
# 7.3.0 (flake8-bugbear: 24.12.12, mccabe: 0.7.0,
# pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.12.3
# make a missing plugin fail loudly
flake8 --require-plugins=flake8-bugbear,flake8-comprehensions .Installing a plugin activates it everywhere in that environment with no opt-in, which is how one developer's virtualenv ends up reporting codes CI never sees. --require-plugins in your config is the fix. flake8 --version is the fastest way to see what is actually loaded.
Flag over-complicated functionscomplexity-check
flake8 --max-complexity=10 src/
# src/handler.py:88:1: C901 'process' is too complex (17)The mccabe check is installed but off by default; it only runs once you set a threshold. C901 counts branches, not lines, so a long but flat function passes while a short one with nested conditionals fails. Ten is the common starting value.
Get useful output in CIci-reporting
flake8 --count --statistics --show-source .
# machine readable, for annotations
flake8 --format='%(path)s:%(row)d:%(col)d: %(code)s %(text)s' .
# report but never fail the build (migration mode)
flake8 --exit-zero --output-file=flake8.txt .--statistics prints a count per error code, which tells you which single rule is responsible for four hundred of your five hundred findings. --exit-zero is the honest way to introduce flake8 to a legacy codebase without blocking every pull request on day one.
Lint a buffer instead of a filelint-stdin
cat src/app.py | flake8 - --stdin-display-name=src/app.py
git diff --name-only --diff-filter=ACM 'HEAD' -- '*.py' \
| xargs --no-run-if-empty flake8The dash reads stdin, and --stdin-display-name is what makes the reported path match the real file so editors can jump to it. flake8 7 removed --diff, so linting only changed lines now means filtering the file list yourself as above.
Work out why a rule is or is not firingdebug-config
flake8 --bug-report # JSON of versions, plugins, platform
flake8 -vv src/app.py # very verbose: config files found, plugins run
flake8 --isolated src/app.py # ignore every config file on disk--isolated is the quickest way to prove that a surprising result comes from a config file somewhere up the tree rather than from the code. --bug-report is what maintainers ask for on issues and it also shows the exact plugin versions in play.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ruff | PyPI | You want the same rules plus most plugin rules, autofix, pyproject.toml config, and a run that finishes in a fraction of the time |
| pylint | PyPI | You want deeper analysis than pyflakes gives, including inference across modules and design warnings, and you can accept slower runs and more false positives |
| pycodestyle | PyPI | You only want the PEP 8 style checks and none of the plugin machinery or error detection |
| pyflakes | PyPI | You only care about real errors such as unused imports and undefined names and want zero style opinions |