The CI/CD Nightmare: Spending 5 Minutes Just to Catch Formatting Issues
If you work on a Python team with a codebase spanning tens of thousands of lines or more, this scenario will sound painfully familiar. Every time someone opens a Pull Request, GitHub Actions spins for 3 to 5 minutes. The pipeline stalls just waiting for a parade of tools running sequentially:
- isort sorts and organizes import statements.
- Black formats indentation, quotes, and line wrapping according to PEP 8.
- Flake8 alongside a maze of plugins (flake8-bugbear, flake8-docstrings…) scans for potential issues.
- pyupgrade checks and upgrades outdated Python syntax.
The result? CI runner costs skyrocket, and engineers waste valuable time waiting on code reviews.
Worst of all is the local developer experience. Because pre-commit hooks take tens of seconds to run, developers often bypass them with git commit --no-verify to save time. Inevitably, the code fails on CI due to trivial trailing whitespace errors, disrupting the team’s entire sprint cadence.
Why the Flake8, Black, and isort Trio Is Slow and Conflict-Prone
There are three core bottlenecks that cause this traditional setup to struggle as projects scale:
- Python runtime overhead: All three tools are written in Python. Every time they run, the operating system must spin up an interpreter and load dozens of libraries into RAM. This startup overhead adds substantial latency.
- Redundant AST parsing: Suppose you have a
services/order.pyfile with 2,500 lines of code. isort reads the file and builds an Abstract Syntax Tree (AST). Next, Black parses the AST a second time. Then Flake8 steps in and parses it a third time. Because these three tools operate independently and share no memory, CPU cycles are squandered repeatedly. - Conflicting formatting rules: Black defaults to an 88-character line length, while Flake8 strictly enforces
E501for lines exceeding 79 characters. When Black adds whitespace before a colon in slice indexing (such asx[1: 2]), Flake8 flags it as anE203violation. Ultimately, you are forced to manage separate.flake8,setup.cfg, andpyproject.tomlconfiguration files just to resolve these needless conflicts.
Band-Aid Solutions We Tried (And Why They Failed)
Before discovering Ruff, our team experimented with several workarounds:
- Splitting jobs to run in parallel on CI: Running isort, Black, and Flake8 concurrently across three separate runners cut wait times from 4 minutes to around 2 minutes. However, our CI bill doubled because it consumed far more runner minutes, and local pre-commit hooks remained just as painfully slow.
- Pruning Flake8 plugins: Disabling heavy rules and retaining only bare-minimum syntax checks marginally improved speed, but the trade-off was costly: unused variables, dangling imports, and subtle logic bugs slipped unnoticed past reviewers straight into the
mainbranch.
The takeaway was crystal clear: instead of stitching together fragmented tools, the definitive solution was consolidating AST parsing into a single, unified system-level engine.
The Definitive Fix: Migrating Everything to Ruff
Ruff is an open-source project created by Charlie Marsh (founder of Astral), built entirely in Rust. It is not just another linter—it is an all-in-one toolkit designed as a drop-in replacement for Flake8, Black, isort, pyupgrade, autoflake, and more than 40 popular plugins.
Ruff is anywhere from 10 to 100 times faster than native Python tools. It achieves this blistering performance through multi-threaded processing with Rayon, zero-copy memory management on the AST, and direct compilation to machine code.
1. Installing Ruff in Your Project
You can install Ruff in seconds using pip, poetry, or uv:
# Install via pip
pip install ruff
# Or add to dev dependencies with Poetry
poetry add --group dev ruff
2. Consolidating Configuration in pyproject.toml
You can safely delete scattered legacy config files like .flake8 or .isort.cfg. All your rules now live cleanly inside pyproject.toml:
[tool.ruff]
# Maximum line length (matches Black's default)
line-length = 88
target-version = "py311"
# Exclude directories from scanning
exclude = [
".git",
".venv",
"__pycache__",
"build",
"dist",
]
[tool.ruff.lint]
# Rule sets to enable:
# E, W: Style errors and warnings (replaces pycodestyle/Flake8)
# F: Unused variables, invalid imports (replaces Pyflakes)
# I: Import sorting (replaces isort)
# B: Potential logic bugs (replaces flake8-bugbear)
# UP: Syntax modernization (replaces pyupgrade)
select = ["E", "W", "F", "I", "B", "UP"]
# Ignore noisy rules or rules handled by the formatter
ignore = ["E501", "B008"]
[tool.ruff.lint.isort]
# Group internal project imports
known-first-party = ["my_project_module"]
combine-as-imports = true
[tool.ruff.format]
# Formatting configuration fully compatible with Black
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
3. Running Checks and Automatic Fixes
Ruff cleanly separates concerns with two main CLI commands:
ruff check: Functions as the Linter (catches unused variables, dead imports, and syntax issues).ruff format: Functions as the Formatter (a 100% drop-in replacement for Black).
To scan and automatically apply safe fixes:
# Scan and automatically apply safe fixes
ruff check . --fix
# Reformat the entire codebase in the blink of an eye
ruff format .
4. Integrating a Blazing-Fast Pre-commit Hook
Update your .pre-commit-config.yaml file to catch issues the moment developers run git commit:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
# Run the Linter and auto-fix issues
- id: ruff
args: [--fix]
# Format code
- id: ruff-format
Pre-commit now executes in just 0.1 to 0.2 seconds. It runs so fast developers barely notice it, putting an end to the habit of skipping checks with --no-verify.
5. Optimizing CI/CD Pipelines on GitHub Actions
On CI, you can strip away legacy boilerplate and replace it with a clean, concise workflow:
name: Code Quality Check
on: [push, pull_request]
jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ruff
run: pip install ruff
- name: Run Ruff Linter
run: ruff check --output-format=github .
- name: Run Ruff Formatter
run: ruff format --check .
In our own production experience across a 65,000-line backend repository, GitHub Actions lint and format times dropped dramatically from 2 minutes and 20 seconds to just 1.4 seconds. Config friction between linter and formatter vanished, and debates over single versus double quotes during code reviews became a thing of the past.

