Linters & formatters

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

Linters & formatters · · Published 2026-10-27

Rows of women at keypunch machines in a large 1940s office, transferring census forms onto punched cards
Keypunch operators transferring 1940 census questionnaires onto punched cards. US Census Bureau, public domain.

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:

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:

LetterCategoryExample
CConventionC0103 invalid-name
RRefactorR0913 too-many-arguments
WWarningW0613 unused-argument
EErrorE1101 no-member
FFatalpylint could not process the file
IInformationnotes 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

flake8pylint
ScopeLayout, likely errors, complexity; more via pluginsErrors, design, naming, conventions, refactoring hints
AnalysisPer file, no type inferenceInference across modules
Default noiseLowHigh until configured
SpeedFastSlower on large codebases
Configurationsetup.cfg, tox.ini, .flake8pyproject.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