DevToys Web Pro iconDevToys Web ProBlog
Diterjemahkan dengan LocalePack logoLocalePack
Nilai kami:
Cuba sambungan pelayar:
← Back to Blog

Python Formatter Guide: Black, autopep8, ruff format, and PEP 8

10 min read

Python has more formatting tools than any other mainstream language, yet teams still argue about line lengths and trailing commas. The good news: the ecosystem has largely converged on two tools — Black and ruff format — both of which take strong opinions so you don't have to. Use the Python Formatter to apply any of these styles directly in your browser before setting up automation locally.

PEP 8 Core Rules That Survive Formatter Debates

PEP 8 is Python's official style guide, published in 2001 and still the foundation every formatter builds on. Most of its rules are uncontroversial and are enforced identically by every tool. Understanding them helps you read formatter output rather than just accepting it.

The rules that have never changed and that all formatters agree on:

  • 4 spaces per indentation level. Never tabs. Python's grammar treats tabs and spaces as distinct, and mixing them raises a TabError at runtime.
  • Two blank lines around top-level definitions. Functions and classes at module level get two blank lines before and after. Methods inside a class get one.
  • Imports at the top, grouped. Standard library first, third-party second, local last. Each group separated by a blank line. No wildcard imports in production code.
  • Whitespace around binary operators. x = a + b, not x=a+b. Exception: no spaces around = in keyword arguments (func(key=value)).
  • No trailing whitespace. Invisible characters that pollute diffs and confuse editors.
  • UTF-8 source encoding. Declared via # -*- coding: utf-8 -*- only if you need to override the default (Python 3 defaults to UTF-8).

These rules are mechanical enough that no formatter debate touches them. Where things get interesting is line length, string quote style, and trailing commas.

Formatter Comparison

Four tools dominate the Python formatting landscape. They share the same goal — consistent, readable code — but differ sharply in philosophy.

ToolPhilosophyConfigurabilitySpeedAdoption
BlackOpinionated, "uncompromising" — minimal options by designVery low — line length and target version onlyFast (Python)Dominant: Django, pytest, Pandas, most major OSS projects
autopep8PEP 8 compliance only — fix violations, don't reformatHigh — select which PEP 8 rules to applyModerateLegacy; common in older codebases and editor integrations
yapfReformats to look like a human wrote it; style is configurableVery high — dozens of style knobsSlow on large filesModerate; used at Google internally
ruff formatBlack-compatible, Rust-based; near-zero configLow — same as Black plus a few extras~30x faster than BlackRapidly growing; preferred for new projects in 2024+

autopep8 and yapf have largely been displaced in new projects. autopep8 only makes the minimum changes required to pass PEP 8 checks, which means a file can pass autopep8 and still look inconsistent across the codebase. yapf produces beautiful output but its configurability is also its weakness — every team configures it differently, and the config becomes another thing to maintain.

Why Black Won

Black calls itself "the uncompromising Python code formatter." That framing is deliberate. The core insight behind Black is that formatting debates are a tax on developer time, and the only way to eliminate them is to remove the options entirely.

Three properties made Black the default choice for most Python projects:

  • Zero config to start. Run black . on any codebase and it works. No style file to create, no rules to choose. Onboarding a new contributor means telling them one thing: "we use Black."
  • Deterministic output. The same input always produces the same output, regardless of who runs it or on which machine. This makes diffs meaningful — a diff is always a real change, never a formatting preference.
  • Minimal diff churn in code review. When Black reformats a file, it reformats it the same way every time. Reviewers stop commenting on whitespace. PRs shrink. Merge conflicts involving formatting disappear.

The psychological shift Black produces is significant. Teams that adopt it report that code review becomes substantially faster because reviewers redirect attention from formatting to logic. That alone justifies the opinionated choices Black makes.

Line Length: The One Debate Black Didn't End

PEP 8 recommends 79 characters. Black defaults to 88. Many teams use 100 or 120. This is the one formatting parameter that everyone actually configures, and for good reason: the right answer depends on your team's monitors and workflow.

LimitOriginTradeoffs
79PEP 8 original (80-column terminal history)Forces aggressive line breaks; comfortable for side-by-side diffs; painful for chained method calls
88Black default (10% wider than 79)Fits most expressions without breaking; 10% fewer line continuations than 79; industry consensus in 2024
100Google style guideComfortable on wide monitors; common in data science codebases
120JetBrains defaultRarely breaks any real expression; can hide deeply nested code that should be refactored

Black's argument for 88 is pragmatic: 79 causes too many forced line breaks in modern Python (f-strings, type annotations, chained calls), and going much past 88 lets bad nesting hide. For most teams, 88 or 100 is the right call. Pick one, put it in pyproject.toml, and stop discussing it.

Configurable Pieces: pyproject.toml

Both Black and ruff format read configuration from pyproject.toml. Keeping formatter config there — rather than in separate .blackrc or setup.cfg files — consolidates all tool configuration in one place.

# pyproject.toml

[tool.black]
line-length = 88
target-version = ["py311", "py312"]
# skip-string-normalization = true  # uncomment to keep single quotes

target-version tells Black which Python syntax is valid in your project. It affects which string concatenations Black will merge, which f-string patterns it recognizes, and so on. Set it to the minimum Python version your project supports.

skip-string-normalization is the most commonly requested Black option. By default Black converts all strings to double quotes. If your codebase has a strong preference for single quotes, this flag preserves them. Most teams leave it off.

# pyproject.toml — ruff format configuration

[tool.ruff.format]
line-length = 88
quote-style = "double"          # "single" | "double" | "preserve"
indent-style = "space"
skip-magic-trailing-comma = false
docstring-code-format = true    # format code blocks inside docstrings

ruff format: When to Pick it Over Black

ruff is a Rust-based Python linter and formatter. Its formatter, introduced in ruff 0.1.0, is intentionally Black-compatible: it produces the same output as Black on the vast majority of real-world code. The main reason to choose ruff format over Black is speed.

On a large monorepo (100,000+ lines), Black may take 10–20 seconds to format all files. ruff format does the same job in under a second. That matters in pre-commit hooks, where every second of hook latency erodes developer discipline.

Other reasons to prefer ruff format in new projects:

  • One tool, one pyproject.toml section for both linting and formatting (ruff replaces flake8, isort, and Black in a single binary).
  • Active development: features like docstring-code-format are available in ruff first.
  • Near-identical output means you can migrate from Black to ruff format in a single reformatting commit with essentially no behavioral diff.

The one reason to stay on Black: if your team has existing Black configuration, CI integrations, and tooling, the migration cost may not be worth the speed gain on smaller codebases.

Import Sorting: isort and ruff I Rules

Black does not sort imports. It formats the lines that are already there but does not reorder them. You need a separate tool for that. The two options are isort and ruff's import rules.

isort is the traditional choice. It groups imports into the three PEP 8 buckets (stdlib, third-party, first-party), sorts alphabetically within each group, and handles edge cases like conditional imports and __future__ imports correctly.

# pyproject.toml — isort, compatible with Black

[tool.isort]
profile = "black"
line_length = 88

The profile = "black" setting is essential. Without it, isort and Black will fight each other — isort reformats imports one way, Black reformats them another, and running both in sequence produces different output on each pass.

If you are already using ruff for linting, enable the I rule set instead:

# pyproject.toml — ruff with import sorting

[tool.ruff.lint]
select = ["E", "F", "I"]   # E/F = pycodestyle + pyflakes, I = isort rules

[tool.ruff.lint.isort]
known-first-party = ["mypackage"]

ruff's import sorting is compatible with isort's profile = "black" behavior. You get import sorting and linting from a single fast binary.

Pre-commit Integration

Pre-commit hooks run formatters automatically before each commit, so malformatted code never reaches the repository. Install pre-commit with pip install pre-commit, then create .pre-commit-config.yaml at the repository root.

# .pre-commit-config.yaml — using Black + isort

repos:
  - repo: https://github.com/psf/black
    rev: 24.10.0
    hooks:
      - id: black
        language_version: python3.12

  - repo: https://github.com/PyCQA/isort
    rev: 5.13.2
    hooks:
      - id: isort
        args: ["--profile", "black"]
# .pre-commit-config.yaml — using ruff (formatter + linter + import sort)

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
      - id: ruff          # linter (runs first)
        args: ["--fix"]
      - id: ruff-format   # formatter (runs after linter)

After creating the config, run pre-commit install to register the hooks. Pre-commit operates only on staged files, so it adds minimal latency to commits regardless of repository size. Run pre-commit run --all-files once to apply formatting to the entire codebase on initial setup.

CI Enforcement

Pre-commit catches formatting issues locally, but CI enforcement catches cases where someone bypassed the hook with --no-verify or committed from a tool that doesn't run hooks. The check mode of both Black and ruff format exits with a non-zero status if any file would be reformatted, which fails the CI job.

# Check mode — exits 1 if any file would change
black --check .
ruff format --check .
# .github/workflows/lint.yml

name: Lint

on: [push, pull_request]

jobs:
  format-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install ruff
        run: pip install ruff

      - name: Check formatting
        run: ruff format --check .

      - name: Check linting
        run: ruff check .

Keep the CI formatter check fast. If you are only checking formatting (not running tests), ruff format --check on a large codebase completes in under two seconds, making it reasonable to run on every push rather than only on pull requests.

Common Pitfalls

Adopting a formatter is straightforward, but several edge cases cause friction that is worth knowing in advance.

  • Docstrings. Black does not reformat the content of docstrings — only the quotes and indentation surrounding them. Long lines inside docstrings are not wrapped. ruff format with docstring-code-format = true handles code blocks inside docstrings, but prose lines remain untouched. Use a separate tool like docformatter if docstring line length is important to you.
  • Long string literals. Neither Black nor ruff format splits a long string literal onto multiple lines. "this is a very long string that exceeds the line limit" stays on one line. The formatter only controls how expressions and function calls wrap; string content is yours to manage.
  • Magic trailing comma. Black and ruff format respect what is called the "magic trailing comma": if you put a trailing comma after the last element in a collection or argument list, the formatter will always expand that structure to one-item-per-line, regardless of whether it would fit on one line. This is intentional and useful for keeping long argument lists expanded, but it surprises teams migrating from other formatters. A diff that removes a trailing comma may unexpectedly collapse a multi-line call.
  • Editor on-save vs format-on-commit conflicts. If your editor formats on save using one configuration and your pre-commit hook uses another (different line length, different quote style), you get an endless loop: the editor formats one way, the hook reformats on commit, the next save reformats again. Ensure your editor's Black or ruff configuration points at the same pyproject.toml that the hook uses.
  • isort and Black fighting. If isort runs without profile = "black", it wraps import lines differently than Black expects. Always set the profile when using both tools, or switch to ruff's built-in import sorting.

Format Python code instantly with the Python Formatter — paste your code, choose your formatter style, and copy the result. For broader formatting topics, see the Code Formatters Guide and the SQL Formatter Guide.