How to Use Flake8 and Black for Python Code Quality and Style Consistency

6 min read

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

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
How to Use Flake8 and Black for Python Code Quality and Style Consistency
Without Code Issue – Based on actual testing on my local machine.

No output means no issues.

What Each Tool Handles

IssueFlake8 Catches ItBlack Fixes It
Missing spaces around operatorsYesYes
Lines too longYesYes (reformats)
Inconsistent indentationYesYes
Missing blank lines between functionsYesYes
Trailing whitespaceYesYes
Unused importsYesNo (manual fix)
Unused variablesYesNo (manual fix)
== True / != NoneYesNo (manual fix)
Syntax errorsYesNo

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 default
  • E203 — 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:

  1. Write code normally — don’t worry about formatting
  2. Run black . to format everything (or let pre-commit do it)
  3. Run flake8 . to check for quality issues that Black doesn’t handle
  4. Fix any remaining Flake8 warnings (unused imports, bad comparisons, etc.)
  5. 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

CodeMeaning
E225Missing whitespace around operator
E231Missing whitespace after ,
E302Expected 2 blank lines before function
E501Line too long
E711Comparison to None (use is / is not)
E712Comparison to True/False (use if x:)
F401Module imported but unused
F841Local variable assigned but unused
W291Trailing whitespace
W503Line 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.