Inconsistent code style is one of those things that doesn’t break anything but slowly makes a codebase harder to read. If you’ve ever opened a pull request and spent more time on formatting comments than actual logic, Flake8 and Black will save you from that. Flake8 catches code quality issues (unused imports, syntax errors, PEP 8 violations), and Black auto-formats your code so every file looks the same.
This guide shows how to set up both tools, configure them to work together, and automate the whole thing with pre-commit hooks. Examples are run on WSL2 Ubuntu in Windows, but they work on any system with Python installed.
Prerequisites
- Python 3.8+ installed — see How to Install and manage Python Versions on WSL Ubuntu if needed
pipavailable (comes with Python)
Install Flake8 and Black
pip install flake8 black
Verify both are installed:
flake8 --version
black --version
Before and After: What Flake8 and Black Actually Do
The best way to understand these tools is to see them in action. Here’s a messy Python file and what it looks like after running both tools.
Before: Messy Code
Save this as example.py:
import os
import sys
import json
from datetime import datetime
def get_user_data(user_id,db_connection,include_metadata=True):
"""Fetch user data from database."""
query="SELECT * FROM users WHERE id = %s"
result=db_connection.execute(query,(user_id,))
if result is None:
return None
data = {"id":user_id,"name":result["name"],"email":result["email"],"created_at":str(result["created_at"])}
if include_metadata==True:
data["fetched_at"]=str(datetime.now())
data["source"]="primary_db"
return data
def process_users(user_ids,db):
results=[]
for id in user_ids:
data=get_user_data(id,db)
if data!=None:
results.append(data)
return results
This code works, but it has several issues: unused imports (os, sys, json), inconsistent spacing, no spaces around operators, a line that’s way too long, and == True / != None instead of idiomatic Python.
Running Flake8 on the Messy Code
flake8 example.py
Output:
example.py:1:1: F401 'os' imported but unused
example.py:2:1: F401 'sys' imported but unused
example.py:3:1: F401 'json' imported but unused
example.py:6:1: E302 expected 2 blank lines before function definition, got 1
example.py:6:25: E231 missing whitespace after ','
example.py:8:10: E225 missing whitespace around operator
example.py:9:11: E225 missing whitespace around operator
example.py:13:5: E501 line too long (109 > 88 characters)
example.py:14:24: E712 comparison to True should be 'if include_metadata:'
example.py:19:1: E302 expected 2 blank lines before function definition, got 1
example.py:20:12: E225 missing whitespace around operator
example.py:22:12: E225 missing whitespace around operator
example.py:23:16: E711 comparison to None should be 'if data is not None:'
Flake8 reports the problems but doesn’t fix them. That’s by design — it’s a linter, not a formatter. You fix the logic issues manually (unused imports, == True), and let Black handle the formatting.

After: Clean Code
First, manually fix what Flake8 found that Black can’t fix — remove unused imports, fix == True to just if include_metadata:, and fix != None to is not None. Then run Black:
black example.py
Output:
reformatted example.py
All done!
1 file reformatted.
The file now looks like this:
from datetime import datetime
def get_user_data(user_id, db_connection, include_metadata=True):
"""Fetch user data from database."""
query = "SELECT * FROM users WHERE id = %s"
result = db_connection.execute(query, (user_id,))
if result is None:
return None
data = {
"id": user_id,
"name": result["name"],
"email": result["email"],
"created_at": str(result["created_at"]),
}
if include_metadata:
data["fetched_at"] = str(datetime.now())
data["source"] = "primary_db"
return data
def process_users(user_ids, db):
results = []
for id in user_ids:
data = get_user_data(id, db)
if data is not None:
results.append(data)
return results
Black handled the formatting: consistent spacing around operators, proper blank lines between functions, and broke the long dictionary onto multiple lines. The unused imports and comparison fixes were the manual part.
Run Flake8 again to confirm everything’s clean:
flake8 example.py

No output means no issues.
What Each Tool Handles
| Issue | Flake8 Catches It | Black Fixes It |
|---|---|---|
| Missing spaces around operators | Yes | Yes |
| Lines too long | Yes | Yes (reformats) |
| Inconsistent indentation | Yes | Yes |
| Missing blank lines between functions | Yes | Yes |
| Trailing whitespace | Yes | Yes |
| Unused imports | Yes | No (manual fix) |
| Unused variables | Yes | No (manual fix) |
== True / != None | Yes | No (manual fix) |
| Syntax errors | Yes | No |
This is why you use both: Black fixes formatting automatically, but Flake8 catches logic and quality issues that a formatter can’t (and shouldn’t) change for you.
Configuring Flake8 and Black to Work Together
Black uses 88 characters as its default line length. Flake8 defaults to 79 (the original PEP 8 limit). If you don’t align them, Black will format a line to 88 characters and Flake8 will complain it’s too long. Not a great experience.
Flake8 Configuration
Create a .flake8 file in your project root:
[flake8]
max-line-length = 88
extend-ignore = E203, W503
max-line-length = 88— matches Black’s defaultE203— whitespace before:(Black formats slices differently than PEP 8 expects)W503— line break before binary operator (Black does this intentionally, PEP 8 now prefers it too)
Black Configuration (Optional)
Black’s defaults are good for most projects. But if you need to customize, add a [tool.black] section to your pyproject.toml:
[tool.black]
line-length = 88
target-version = ["py311"]
The target-version tells Black which Python version you’re targeting so it doesn’t use syntax features your runtime doesn’t support.
Using Flake8 and Black
Running Flake8
Check a file or directory:
# Single file
flake8 app.py
# Entire project
flake8 src/
# Show statistics summary
flake8 src/ --statistics
Running Black
# Format a file (modifies in place)
black app.py
# Format a directory
black src/
# Preview changes without writing (dry run)
black --check app.py
# Show what would change
black --diff app.py
The --check flag is useful in CI/CD — it exits with a non-zero code if formatting is needed, failing the pipeline without modifying files.
Automate with Pre-Commit Hooks
Running Flake8 and Black manually gets old fast. The better approach is to run them automatically before every commit using pre-commit hooks.
pip install pre-commit
Create .pre-commit-config.yaml in your project root:
repos:
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 7.1.1
hooks:
- id: flake8
Black runs first so files are formatted before Flake8 checks them. Install the hooks and test:
pre-commit install
pre-commit run --all-files
Now every git commit will automatically format with Black and lint with Flake8. If either tool fails, the commit is blocked until you fix the issues.
The Workflow
The day-to-day workflow is simple:
- Write code normally — don’t worry about formatting
- Run
black .to format everything (or let pre-commit do it) - Run
flake8 .to check for quality issues that Black doesn’t handle - Fix any remaining Flake8 warnings (unused imports, bad comparisons, etc.)
- Commit — pre-commit hooks catch anything you missed
Once pre-commit is set up, steps 2-4 happen automatically. You just write code and commit.
Common Flake8 Codes You’ll See
| Code | Meaning |
|---|---|
E225 | Missing whitespace around operator |
E231 | Missing whitespace after , |
E302 | Expected 2 blank lines before function |
E501 | Line too long |
E711 | Comparison to None (use is / is not) |
E712 | Comparison to True/False (use if x:) |
F401 | Module imported but unused |
F841 | Local variable assigned but unused |
W291 | Trailing whitespace |
W503 | Line break before binary operator |
Conclusion
Flake8 and Black together eliminate formatting debates and catch common mistakes before they get into your codebase. Set them up once with pre-commit, and you don’t have to think about code style again. The team just writes code and everything comes out consistent.
For a deeper setup on pre-commit hooks, check How to Install and Use Pre-commit on Ubuntu WSL 2. And if you’re organizing a growing Python project, How to Structure Your Python Projects for AWS Lambda, APIs, and CLI Tools covers project layout and structure.


