Skip to content

Python Code Quality: Black, Flake8, and Ruff

Python's flexibility is a feature, but that same flexibility means every team is one undocumented style preference away from a code review thread about trailing whitespace. Linters and formatters exist to take those conversations off the table and keep them there.

Three tools dominate the Python ecosystem today: Black, the opinionated formatter; Flake8, the composable linting wrapper; and Ruff, the Rust-powered all-in-one tool that has become the default choice for new projects. Each takes a different approach to the same goal of keeping Python code clean, consistent, and readable.

This post covers what each tool does, the trade-offs involved in choosing between them, who is behind each project, and why Ruff has become the tool to reach for on a greenfield Python project in 2026.

The Problem These Tools Solve

Python has an official style guide (PEP 8), but it leaves a lot of room for interpretation. Indentation is specified (4 spaces), but line length is only recommended (79 characters per PEP 8, though 88 is widely used in practice following Black's default). Import grouping is addressed (standard library, third-party, local), but the sort order within each group is not. And nothing stops two developers on the same team from making genuinely different but equally PEP-8-compliant choices that create noise in every diff.

Linters and formatters solve this by making the rules authoritative and automatic. Linters report violations; formatters fix them. The distinction matters when choosing a tool.

Black: The Uncompromising Formatter

What It Does

Black is a code formatter, not a linter. It does not report problems. It rewrites your code to a single, canonical style and exits. You cannot configure most of its decisions. That is the point.

Black handles whitespace, trailing commas, string normalization (double quotes by default), line length (default 88 characters), and the formatting of complex expressions. It does not touch import order, variable names, or logic.

# Before
x = {'a':1,'b':  2,'c':3}
long_list = ['item_one','item_two','item_three','item_four','item_five','item_six']

# After Black
x = {"a": 1, "b": 2, "c": 3}
long_list = [
    "item_one",
    "item_two",
    "item_three",
    "item_four",
    "item_five",
    "item_six",
]

Who Is Behind It

Black is maintained by the Python Software Foundation (PSF) as a first-party project under the psf/black repository on GitHub. It started as a personal project by Łukasz Langa (CPython core developer and Python Release Manager for 3.8 and 3.9) and was transferred to the PSF's GitHub organization in 2021. Active maintenance and releases continue from a team of contributors under PSF governance.

Pros

  • Zero configuration friction. There is no style debate because there are no style options. The formatter makes every decision.
  • Deterministic output. Given the same input, Black always produces the same output, regardless of environment, for a given Black version.
  • PSF-backed. Being an official PSF project means long-term maintenance is credible and the project is unlikely to be abandoned.
  • Broad ecosystem support. Black integration exists in every major editor, CI platform, and pre-commit hook registry.
  • Format-on-save integration is mature and well documented for VS Code, Neovim, PyCharm, and Emacs.

Cons

  • Formatter only. Black does not catch logic errors, unused imports, undefined variables, or any of the hundreds of rules that a linter enforces. You still need a linter alongside it.
  • Opinionated to a fault for some teams. Most of Black's formatting decisions cannot be changed. String quote style (skip-string-normalization) and magic trailing comma handling (skip-magic-trailing-comma) are configurable via pyproject.toml, but they are the rare exceptions. Teams adopting Black on an existing large codebase should expect a significant initial reformatting commit.
  • Slow on large codebases. Black is written in Python. On repositories with tens of thousands of files, formatting passes can take minutes.
  • No import sorting. You need isort or equivalent as a separate tool to handle import ordering.

Flake8: The Composable Linting Wrapper

What It Does

Flake8 is a linting framework, not a formatter. It wraps three underlying tools: pycodestyle (PEP 8 style checks), pyflakes (logical error detection), and mccabe (cyclomatic complexity). It reports violations with error codes (like E501 for line too long, F401 for unused import) but does not fix them.

$ flake8 my_module.py
my_module.py:4:1: F401 'os' imported but unused
my_module.py:12:80: E501 line too long (92 > 79 characters)
my_module.py:20:5: E303 too many blank lines (3)

Flake8's real power is its plugin ecosystem. Plugins like flake8-bugbear, flake8-type-checking, flake8-bandit (security), and pep8-naming can extend it with hundreds of additional rules.

Who Is Behind It

Flake8 lives under the PyCQA (Python Code Quality Authority) organization on GitHub. PyCQA is an informal collective of maintainers responsible for pycodestyle, pyflakes, isort, pylint, bandit, and several other tools. It is not a formal foundation; it operates as a volunteer-driven community organization. Flake8's primary maintainers have historically been a small group of volunteers, which has occasionally led to slow release cycles and delayed support for new Python versions.

Pros

  • Battle-tested. Flake8 has been the standard Python linting tool for over a decade. Documentation, tutorials, and CI examples are everywhere.
  • Extensible. The plugin ecosystem is unmatched. If you need a specialized check, a plugin probably already exists. If not, writing one is straightforward.
  • Fine-grained control. Rules can be enabled, disabled, or configured per file using inline # noqa comments or a .flake8 configuration file.
  • Pairs well with Black. A common setup is Black for formatting and Flake8 (with flake8-bugbear) for linting. Many teams have run this combination for years.

Cons

  • No auto-fix. Flake8 reports problems but does not fix them. autopep8 exists as a companion fixer, but it is a separate tool with incomplete coverage and its own configuration.
  • Plugin ecosystem fragmentation. Because plugins are independent packages, version conflicts between Flake8 and its plugins are common. Upgrading one often breaks another.
  • No type annotation support built in. You need additional plugins for type-related checks.
  • Volunteer-only maintenance. The informal PyCQA structure means that critical bugs or Python version support can lag when maintainers are unavailable.
  • Configuration sprawl. With many plugins installed, the configuration file grows quickly and becomes difficult to reason about.
  • Performance. Flake8 is Python calling Python. On large codebases it is slow, and parallel execution via the --jobs flag has limited and inconsistent behavior across environments.

Ruff: The Modern All-in-One

What It Does

Ruff is a Python linter and formatter written in Rust. It reimplements rules from Flake8, pycodestyle, pyflakes, isort, pydocstyle, pyupgrade, flake8-bugbear, and dozens of other tools into a single binary. It also includes a formatter that is intentionally compatible with Black's output.

$ ruff check my_module.py
my_module.py:4:1: F401 [*] `os` imported but unused
my_module.py:12:80: E501 Line too long (92 > 88)
Found 2 errors.
[*] 2 fixable with the `--fix` option.

$ ruff check --fix my_module.py
Found 2 errors (2 fixed, 0 remaining).

$ ruff format my_module.py
1 file reformatted.

Ruff is configured in pyproject.toml, ruff.toml, or .ruff.toml. It supports rule selection with the same codes as Flake8, so migration from an existing Flake8 configuration is largely mechanical.

# pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py313"

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
ignore = ["E501"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

Who Is Behind It

Ruff was created by Charlie Marsh and is developed and maintained by Astral, the company Marsh founded specifically to build high-performance Python tooling. Astral also maintains uv, the fast Python package and project manager. The company has received venture funding, has a dedicated full-time engineering team, and releases frequently.

This distinction is worth considering: Ruff is not a volunteer side project. It is a commercially funded product with an open-source license (MIT). The trade-off is that the project's direction is ultimately controlled by a private company rather than a community foundation or collective, though the roadmap is publicly discussed on GitHub and community input is actively solicited.

Pros

  • Exceptional performance. Ruff is 10-100x faster than equivalent Flake8 or Black runs on large codebases. On a repository with 100,000 lines of Python, Ruff typically completes in under a second.
  • Single tool for linting and formatting. Replace Black, isort, Flake8, and multiple plugins with one dependency and one configuration block.
  • Auto-fix. Most fixable rules can be corrected automatically with --fix. This includes unused imports, incorrect import order, deprecated syntax, and more.
  • Black-compatible formatter. Ruff's formatter produces output that is, by design, consistent with Black's formatting decisions, so migration from Black is low-friction.
  • Active, funded development. Astral's full-time team ships frequent releases, adds new rules rapidly, and responds to issues quickly. Python 3.13 support was available almost immediately after Python 3.13's release.
  • Excellent pyproject.toml integration. Single-file project configuration is a first-class design goal, matching the direction of the broader Python packaging ecosystem.
  • Pre-commit hooks and editor integrations are well maintained and widely documented.

Cons

  • Commercially backed, not community governed. Astral controls the roadmap. If Astral pivots, is acquired, or shuts down, the project's future is less certain than a PSF or PyCQA project would be. The MIT license preserves the option to fork if the project's direction changes, but commercial dependency is still a risk worth factoring in for long-lived projects.
  • Plugin ecosystem does not exist (yet). Ruff's rules are built in. If you depend on a specialized Flake8 plugin that Ruff has not reimplemented, you may need to keep Flake8 around for that check.
  • Formatter is still maturing. The Ruff formatter reached stable status in version 0.2.0 (early 2024), but it is younger than Black. Edge cases exist, and the style decisions are not quite as battle-tested.
  • Rust dependency for contributors. If you want to contribute a new rule or fix a bug, you need to know Rust. Python contributors to the linting ecosystem may find the contribution bar higher.

Side-by-Side Comparison

Capability Black Flake8 Ruff
Formatter Yes No Yes (Black-compatible)
Linter No Yes Yes
Auto-fix Yes (format) No Yes (lint + format)
Import sorting No Plugin (flake8-isort) Built-in (I ruleset)
Type annotation rules No Plugin (flake8-type-checking) Built-in (TCH ruleset)
Security checks No Plugin (flake8-bandit) Built-in (S ruleset)
Config file pyproject.toml .flake8 / setup.cfg pyproject.toml
Performance Moderate Slow Excellent
Plugin ecosystem None Extensive Built-in rules only
Backed by PSF PyCQA (volunteers) Astral (commercial)
License MIT MIT MIT

Trade-Off Summary

When Black + Flake8 Still Makes Sense

  • Your team has an established Black + Flake8 setup that is working and you are not experiencing pain.
  • You depend on a Flake8 plugin that Ruff has not implemented (check Ruff's rule index to verify).
  • Your organization's policy requires tools backed by a community foundation rather than a commercial entity.
  • You have contributors who are familiar with the Flake8 plugin API and plan to write custom rules.

When Ruff Is the Right Choice

  • You are starting a new Python project and want a single, fast, well-documented tool.
  • You are consolidating a fragmented toolchain (Black + isort + Flake8 + multiple plugins) and want to simplify.
  • Your codebase is large and linting performance is causing friction in CI pipelines.
  • You want auto-fix support built into your linting workflow, not just your formatter.
  • Your team is already using uv or other Astral tooling and wants a consistent ecosystem.

An Opinionated Take

For any new Python project started today, the default choice should be Ruff. The tool consolidates what used to be four or five separate dependencies into one, runs fast enough that it does not add meaningful time to a pre-commit hook or a CI job, and the configuration model in pyproject.toml fits naturally alongside [tool.pytest.ini_options], [tool.mypy], and the rest of the modern Python project configuration story.

The commercial backing from Astral is a genuine trade-off to weigh. It is not a reason to avoid Ruff, but it is a reason to stay informed. Ruff's MIT license means that any fork remains possible if the project's direction changes, and the PyCQA ecosystem will not disappear if you ever need to migrate back. The practical risk is low.

For existing projects running Black and Flake8, there is no urgent reason to migrate unless you are experiencing performance problems or configuration complexity. Ruff's migration guide is thorough and the tooling equivalence is close, but unnecessary churn in a working toolchain is its own cost.

GitHub Actions Integration

Ruff

name: Lint and Format

on:
  push:
    branches:
      - main
  pull_request:

permissions:
  contents: read

jobs:
  ruff:
    name: Ruff
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.13"
      - name: Install Ruff
        run: pip install ruff
      - name: Run Ruff Lint
        run: ruff check .
      - name: Run Ruff Format Check
        run: ruff format --check .

Alternatively, use Ruff's official GitHub Action:

name: Lint and Format

on:
  push:
    branches:
      - main
  pull_request:

permissions:
  contents: read

jobs:
  ruff:
    name: Ruff
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Run Ruff
        uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1

Black + Flake8

name: Lint and Format

on:
  push:
    branches:
      - main
  pull_request:

permissions:
  contents: read

jobs:
  black:
    name: Black
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.13"
      - name: Install Black
        run: pip install black
      - name: Run Black Check
        run: black --check .

  flake8:
    name: Flake8
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.13"
      - name: Install Flake8
        run: pip install flake8 flake8-bugbear
      - name: Run Flake8
        run: flake8 .

Pre-Commit Configuration

For teams using pre-commit, both approaches are well supported. If you are new to pre-commit or want a deeper look at setting it up across a project, see Elevate Your Git Workflow: A Guide to Using pre-commit.

Ruff

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.7
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

Black + Flake8

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/psf/black
    rev: 26.3.1
    hooks:
      - id: black

  - repo: https://github.com/PyCQA/flake8
    rev: 7.3.0
    hooks:
      - id: flake8

Summary

Black Flake8 Ruff
Best for Teams that want no style debates Teams that need flexible, extensible linting New projects; teams consolidating tooling
Maturity Stable Very stable Rapidly maturing
Governance PSF (community) PyCQA (volunteers) Astral (commercial)
Recommended for new projects Pair with Flake8 Pair with Black Yes, standalone