flake8 Best Practices
flake8 is your essential Python linter, combining pycodestyle, pyflakes, and mccabe complexity checks with a powerful plugin ecosystem. It ensures your codebase adheres to PEP 8, catches common bugs, and maintains high quality. This guide outlines the definitive approach to using flake8 effectively.
1. Configuration: The Single Source of Truth
Always configure flake8 at the repository root using pyproject.toml. This centralizes tooling configuration and ensures consistent behavior across all environments. Avoid .flake8 or setup.cfg for new projects.
✅ GOOD: pyproject.toml
# pyproject.toml
[tool.flake8]
max-line-length = 88 # Black's default, ensures compatibility
extend-ignore = [
"E203", # Black compatibility: whitespace before ':'
"W503", # Black compatibility: line break before binary operator
"E501", # Ignore line length for specific files/legacy (use sparingly)
]
exclude = [
".git",
"__pycache__",
"build",
"dist",
"venv",
".venv",
"*.egg-info",
"docs", # Exclude documentation build artifacts
"migrations", # Django migrations often have long lines
]
max-complexity = 10 # Enforce reasonable function complexity
per-file-ignores = [
"src/legacy_module.py:E501,F401", # Ignore specific errors in legacy files
"tests/*:S101", # Allow assert for tests (flake8-bandit)
]
# Enable plugins (install them first: pip install flake8-bandit flake8-bugbear flake8-mypy)
enable-extensions = ["B", "B9", "S", "M"] # Bugbear, Bandit, Mypy
❌ BAD: .flake8 (Outdated)
# .flake8 (Avoid this for new projects)
[flake8]
max-line-length = 88
ignore = E203,W503
exclude = .git,__pycache__,build,dist,venv,.venv,*.egg-info,docs,migrations
max-complexity = 10
2. Core Checks: What flake8 Enforces
flake8 combines pycodestyle (PEP 8 style), pyflakes (common bugs), and mccabe (cyclomatic complexity).
a. pycodestyle (E/W codes) - Style Enforcement
E501: Line Too Long
Keep lines concise for readability. Pair flake8 with black or ruff format for automatic formatting.
❌ BAD
def calculate_complex_value(param1, param2, param3, param4, param5, param6, param7, param8, param9, param10):
return (param1 + param2) * (param3 - param4) / (param5 + param6) - (param7 * param8) + param9 - param10 # E501
✅ GOOD
def calculate_complex_value(
param1, param2, param3, param4, param5,
param6, param7, param8, param9, param10
):
intermediate_sum = (param1 + param2) * (param3 - param4)
intermediate_diff = (param5 + param6) - (param7 * param8)
return intermediate_sum / intermediate_diff + param9 - param10
b. pyflakes (F codes) - Bug Detection
F401: Unused Import Remove unused imports to keep your code clean and prevent circular dependencies.
❌ BAD
import os # F401
import sys
def greet(name):
print(f"Hello, {name}!")
✅ GOOD
import sys
def greet(name):
print(f"Hello, {name}!")
c. mccabe (C901) - Cyclomatic Complexity
High cyclomatic complexity indicates a function is doing too much. Refactor complex functions into smaller, focused units. Aim for max-complexity = 10.
❌ BAD
def process_data(data): # C901
if data is None:
return None
if not isinstance(data, list):
return []
processed = []
for item in data:
if isinstance(item, dict):
if "value" in item and item["value"] > 0:
processed.append(item["value"] * 2)
elif "default" in item:
processed.append(item["default"])
elif isinstance(item, int) and item % 2 == 0:
processed.append(item / 2)
else:
processed.append(item)
return processed
✅ GOOD
def _process_dict_item(item):
if "value" in item and item["value"] > 0:
return item["value"] * 2
if "default" in item:
return item["default"]
return None # Or raise an error, or return original item
def _process_int_item(item):
if item % 2 == 0:
return item / 2
return item
def process_data(data):
if data is None:
return None
if not isinstance(data, list):
return []
processed = []
for item in data:
if isinstance(item, dict):
result = _process_dict_item(item)
if result is not None:
processed.append(result)
elif isinstance(item, int):
processed.append(_process_int_item(item))
else:
processed.append(item)
return processed
3. Essential Plugins: Extend flake8's Power
Always install and enable these plugins for a robust linting setup.
flake8-bandit(S codes): Security linter. Catches common security issues.flake8-bugbear(B/B9 codes): Catches common "bear" bugs and design problems.flake8-mypy(M codes): Integratesmypytype checking intoflake8output.
Installation:
pip install flake8 flake8-bandit flake8-bugbear flake8-mypy
Configuration (already in pyproject.toml example):
[tool.flake8]
enable-extensions = ["B", "B9", "S", "M"]
Example: flake8-bandit (S101 - assert statement)
❌ BAD
def check_password(password):
assert password is not None, "Password cannot be None" # S101
# ... security checks ...
✅ GOOD
def check_password(password):
if password is None:
raise ValueError("Password cannot be None")
# ... security checks ...
4. Workflow Integration: Lint Early, Lint Often
Integrate flake8 into your development workflow to catch issues as early as possible.
a. Local Development (IDE Integration)
Configure your IDE (e.g., Cursor, VS Code) to run flake8 on file save. This provides immediate feedback.
b. Pre-commit Hooks
Use pre-commit to automatically run flake8 (and formatters like black or ruff format) before every commit. This ensures no unlinted code ever reaches your repository.
pyproject.toml (or .pre-commit-config.yaml)
# .pre-commit-config.yaml
repos:
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0 # Use the latest stable version
hooks:
- id: flake8
args: ["--config=pyproject.toml"] # Ensure flake8 uses your pyproject.toml
c. CI/CD Pipelines
Include flake8 as a mandatory step in your CI pipeline. If flake8 reports any errors, the build must fail. This acts as a quality gate.
5. Ignoring Errors: Be Specific, Not Lazy
Only ignore errors when absolutely necessary, and always be specific.
❌ BAD: Blanket Ignore
import os, sys # noqa
✅ GOOD: Specific Ignore
import os # noqa: F401,E402
import sys
✅ GOOD: File-level Ignore (in pyproject.toml)
[tool.flake8]
per-file-ignores = [
"src/legacy_module.py:E501,F401",
]
6. Type Hints: Leverage flake8-mypy
While mypy is the primary type checker, flake8-mypy integrates type checking results into your flake8 output, providing a unified view of code quality.
❌ BAD
def add(a, b): # No type hints
return a + b
✅ GOOD
def add(a: int, b: int) -> int: # M501 (flake8-mypy will check these)
return a + b
7. Virtual Environments: Isolate Dependencies
Always install flake8 and its plugins within a project-specific virtual environment. This prevents dependency conflicts and ensures consistent behavior.
python -m venv .venv
source .venv/bin/activate
pip install flake8 flake8-bandit flake8-bugbear flake8-mypy
8. Code Organization & Structure
flake8 indirectly encourages good structure by enforcing PEP 8 and catching issues.
- Separate Concerns: Run
flake8onsrc/andtests/directories. Excludedocs/ormigrations/as needed. - Import Order: While
flake8itself doesn't enforce strict import order,isort(orruff format) does. Pairflake8withisortfor this.- Standard library
- Third-party
- Local application
- Avoid Circular Dependencies:
pyflakes(part offlake8) will warn about these.
9. Common Pitfalls & Gotchas
- Outdated Configuration: Always use
pyproject.toml. - Ignoring too much: A long
ignorelist hides real issues. Fix the code, don't ignore the linter. - Not running
flake8everywhere: Ensureflake8runs on all relevant Python files (src,tests). - Mixing
flake8withblackwithoutE203, W503ignores: This will lead to conflicts. Always configureflake8forblackcompatibility.
By following these guidelines, your team will maintain a consistent, high-quality Python codebase, reducing technical debt and improving collaboration.