flake8 and pylint: What Each Python Linter Checks and How They Differ

flake8 and pylint are the two longest-established Python linters, and they are often mentioned together as if they did the same job. They overlap, but they are built differently and look for different things. Running both on the same file shows it quickly.
The same file, two results
This module is valid, PEP 8-compliant Python. It has no layout problems, but it has some issues a careful reviewer would raise:
"""Inventory helpers."""
import json
def load(path):
"""Load stock levels from a JSON file."""
with open(path) as fh:
data = json.load(fh)
return data
def restock(stock, item, amount, warehouse, supplier, priority):
"""Add amount units of item to stock."""
if item in stock.keys():
stock[item] += amount
else:
stock[item] = amount
return stock.get(item).upper_limit
flake8 reported nothing. pylint reported six messages and a score:
inventory.py:7:9: W1514: Using open without explicitly specifying an encoding (unspecified-encoding)
inventory.py:12:0: R0913: Too many arguments (6/5) (too-many-arguments)
inventory.py:12:0: R0917: Too many positional arguments (6/5) (too-many-positional-arguments)
inventory.py:12:33: W0613: Unused argument 'warehouse' (unused-argument)
inventory.py:12:44: W0613: Unused argument 'supplier' (unused-argument)
inventory.py:12:54: W0613: Unused argument 'priority' (unused-argument)
Your code has been rated at 4.00/10
Neither tool noticed that stock.get(item).upper_limit is almost certainly a bug, since stock values here are numbers. That needs type information: a type checker such as mypy, with annotations, would catch it. Linters and type checkers are complementary.
flake8: a thin layer over focused tools
flake8's documentation describes it as a tool that "glues together" pycodestyle, pyflakes and mccabe, plus any installed plugins. Each component keeps its own code prefix:
- E and W: pycodestyle's layout checks (see our reference to the codes).
- F: pyflakes, which finds errors without importing the code: unused imports and variables, undefined names, redefinitions, f-strings without placeholders.
- C90: mccabe's cyclomatic complexity check, active only when you set
max-complexity.
Plugins add more prefixes, for example N for pep8-naming, B for flake8-bugbear (likely bugs and design problems) or D for docstrings through flake8-docstrings. You install them with pip, and flake8 picks them up automatically.
Configuration lives in an INI-style section of setup.cfg, tox.ini or .flake8; flake8 does not read pyproject.toml natively.
[flake8]
max-line-length = 88
max-complexity = 10
extend-ignore = E203
per-file-ignores =
__init__.py: F401
Use extend-ignore and extend-select to adjust the defaults rather than ignore and select, which replace them. A # noqa: F401 comment silences one code on one line.
pylint: deeper analysis, more opinions
pylint builds a model of your code with its astroid library and infers what values variables can hold, which lets it find problems flake8 cannot see, such as calling a method that does not exist on an object (no-member). It also checks design limits (too many arguments, branches or local variables), naming against configurable patterns, and a long list of error-prone patterns.
Each message has a symbolic name and a code whose letter gives its category:
| Letter | Category | Example |
|---|---|---|
| C | Convention | C0103 invalid-name |
| R | Refactor | R0913 too-many-arguments |
| W | Warning | W0613 unused-argument |
| E | Error | E1101 no-member |
| F | Fatal | pylint could not process the file |
| I | Information | notes such as suppressed messages |
pylint's defaults differ from PEP 8 tools in places: its default max-line-length is 100, and it treats module-level assignments as constants that should be UPPER_CASE, as our naming guide shows. Configuration can live in pyproject.toml under [tool.pylint.*] tables; pylint --generate-toml-config prints every option with its default. Messages can be disabled globally, per file or per line with comments such as # pylint: disable=unused-argument, using the symbolic name so the comment explains itself.
The score, out of 10, is computed from the number of messages relative to the number of statements. It can be useful for tracking a trend, but it is not a quality measure in itself; most teams care about specific messages being at zero.
Running them, and running both
Both tools take files or directories and exit with a non-zero status when they report something, which is what CI needs:
flake8 src/ tests/
pylint src/mypackage
pylint --disable=all --enable=unused-argument,unspecified-encoding src/
The last form is a practical way to introduce pylint on an existing project: start with a short list of messages you agree on, fix them, and widen the list over time, instead of facing hundreds of messages on day one. pylint's exit code is a bit field that encodes which categories of message were emitted, so a script can, for example, fail only on errors.
Projects that run both tools usually disable the overlap in one of them, so the same problem is not reported twice under two names. Line length (pylint's line-too-long, C0301, and pycodestyle's E501) and unused imports (unused-import, W0611, and pyflakes' F401) are the usual duplicates. Keep each check in the tool that runs earliest and fastest, typically flake8 in the editor and in pre-commit, and let pylint focus on what only it sees.
Choosing
| flake8 | pylint | |
|---|---|---|
| Scope | Layout, likely errors, complexity; more via plugins | Errors, design, naming, conventions, refactoring hints |
| Analysis | Per file, no type inference | Inference across modules |
| Default noise | Low | High until configured |
| Speed | Fast | Slower on large codebases |
| Configuration | setup.cfg, tox.ini, .flake8 | pyproject.toml, .pylintrc, setup.cfg |
A common pattern is to run the fast tool on every save and commit, and the deeper one in CI. Since 2022, many projects have replaced flake8 and its plugins with ruff, which implements most of the same rules with the same codes and also covers some pylint checks under its PL prefix; see our guide to ruff. Whatever you choose, configure it once in the repository and treat its output as a shared standard, not personal preference.
For a quick check of layout only, the checker on our home page covers a subset of the E and W codes in the browser.
Sources
- flake8 documentation: "Using Flake8", "Configuring Flake8", "Selecting and Ignoring Violations".
- pylint documentation: message categories, "Messages control", configuration.
- pycodestyle documentation.
- Output above produced with flake8 7.4 and pylint 4.1 on Python 3.13.
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 & 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.
PEP 8, section by sectionNaming conventions
snake_case for functions, CapWords for classes, capitals for constants, and what one or two underscores really do, with the tools that check names.