|$ curl https://forge-ai.dev/api/markdown?path=docs/python/linting
$cat docs/python-—-linting-&-formatting.md
updated Recently·12 min read·published

Python — Linting & Formatting

PythonIntermediate
Introduction

Linting and formatting enforce consistent code style, catch bugs before runtime, and keep codebases maintainable as teams grow. The Python ecosystem has converged on a powerful toolchain: ruff replaces flake8, isort, and many other tools in a single fast binary. black remains the standard formatter, and mypy/pyright handle static type checking.

This guide covers PEP 8 conventions, ruff for linting and formatting, black and isort as alternatives, type checkers, pre-commit hooks, editor integration, and CI pipelines. All configuration lives in pyproject.toml — no more scattered .flake8 and .isort.cfg files.

ToolPurposeReplaces
ruffLint + Formatflake8, isort, pycodestyle, pyflakes, bandit
blackCode formattingautopep8, yapf
mypyStatic type checking
pyrightStatic type checkingmypy (faster, stricter)
pre-commitGit hook automationhusky, lint-staged (JS)
PEP 8 — Style Guide Fundamentals

PEP 8 is the official style guide for Python code. It defines naming conventions, indentation rules, import ordering, whitespace rules, and comment formatting. Tools like ruff and black automate enforcement so you never have to manually check these rules.

RuleConvention
Indentation4 spaces (no tabs)
Line Length79 chars (code), 72 chars (docstrings)
Namingsnake_case functions/vars, PascalCase classes, UPPER_SNAKE constants
ImportsOne per line, grouped (stdlib, third-party, local)
Blanks2 lines before top-level, 1 between methods
QuotesSingle or double (be consistent)
pep8_examples.py
Python
1# ✅ Good PEP 8 style
2from pathlib import Path
3
4MAX_RETRIES = 3
5
6
7def calculate_total(items: list[dict], tax_rate: float = 0.08) -> float:
8 """Calculate total price with tax applied to each item."""
9 subtotal = sum(item["price"] * item["quantity"] for item in items)
10 return subtotal * (1 + tax_rate)
11
12
13class UserProfile:
14 """Represents an authenticated user."""
15
16 def __init__(self, name: str, email: str) -> None:
17 self.name = name
18 self.email = email
19
20 def greet(self) -> str:
21 return f"Hello, {self.name}!"
22
23
24# ❌ Bad style
25from os import*;from pathlib import *
26MAX_retries=3
27def Calculate_Total(items,tax_rate=.08):
28 subtotal=sum( [i['price']*i['quantity'] for i in items])
29 return subtotal*(1+ tax_rate)
30class user_profile:
31 def __init__(self,name,email):
32 self.name=name
33 self.email=email

info

ruff check --select E,W,F enforces the core PEP 8 rules (pycodestyle errors/warnings + pyflakes). For stricter checks, add I (isort) and N (naming) to your rule selection.
Ruff — Lint & Format

ruff is an extremely fast Python linter and formatter written in Rust. It replaces flake8, isort, pycodestyle, pyflakes, and dozens of plugins — all in a single tool that runs 10-100x faster. It also includes a formatter compatible with black.

untitled.bash
Bash
1# Install ruff
2pip install ruff
3
4# Lint the project
5ruff check .
6
7# Lint and auto-fix
8ruff check --fix .
9
10# Format the project
11ruff format .
12
13# Format check only (CI)
14ruff format --check .

Configure ruff entirely in pyproject.toml. The [tool.ruff] section controls line length, target Python version, rule selection, and per-file overrides.

pyproject.toml
TOML
1[tool.ruff]
2target-version = "py312"
3line-length = 88 # Black-compatible default
4
5[tool.ruff.lint]
6select = [
7 "E", # pycodestyle errors
8 "W", # pycodestyle warnings
9 "F", # pyflakes
10 "I", # isort
11 "N", # pep8-naming
12 "UP", # pyupgrade
13 "B", # flake8-bugbear
14 "A", # flake8-builtins
15 "C4", # flake8-comprehensions
16 "SIM", # flake8-simplify
17 "TCH", # flake8-type-checking
18 "RUF", # Ruff-specific rules
19]
20ignore = [
21 "E501", # line too long (handled by formatter)
22]
23
24[tool.ruff.lint.per-file-ignores]
25"tests/**/*.py" = ["S101"] # allow assert in tests
26"__init__.py" = ["F401"] # allow unused imports
27
28[tool.ruff.lint.isort]
29known-first-party = ["myapp"]
30
31[tool.ruff.format]
32quote-style = "double"
33indent-style = "space"
34line-ending = "auto"
🔥

pro tip

Run ruff check --select ALL periodically to discover new rules you might want to enable. The UP (pyupgrade) category is especially useful for modernizing old Python syntax automatically.
Black — Opinionated Formatter

black is the most widely used Python formatter. It enforces a single, opinionated style — no debates about quote style or trailing commas. If you prefer ruff format, it produces nearly identical output and is significantly faster. Use black if your team already has it in place, or switch to ruff format for speed.

untitled.bash
Bash
1# Install
2pip install black
3
4# Format all files
5black .
6
7# Check without modifying
8black --check .
9
10# Format a single file
11black src/my_module.py
12
13# Show diff without applying
14black --diff .
pyproject.toml
TOML
1# pyproject.toml — Black config (skip if using ruff format)
2[tool.black]
3line-length = 88
4target-version = ["py312"]
5include = "\.pyi?$"
6exclude = '''
7/(
8 \bz\b
9 | \bbuild\b
10 | \bdist\b
11 | \bnode_modules\b
12 | \b\.eggs\b
13)/
14'''
📝

note

If you use ruff format, you do not need black. They are mutually exclusive formatters. Pick one — ruff format is recommended for new projects.
isort — Import Sorting

isort automatically sorts and groups imports according to PEP 8. ruff check --select I provides identical functionality as part of ruff. If you use ruff, you do not need a separate isort installation.

untitled.bash
Bash
1# Using ruff (recommended)
2ruff check --select I --fix .
3
4# Using standalone isort
5pip install isort
6isort .
7isort --check-only . # CI mode
import_sorting.py
Python
1# Before isort
2import json
3from pathlib import Path
4import os
5from myapp.models import User
6import sys
7from myapp.utils import format_date
8from collections import OrderedDict
9import requests
10
11# After isort (stdlib → third-party → local)
12import json
13import os
14import sys
15from collections import OrderedDict
16from pathlib import Path
17
18import requests
19
20from myapp.models import User
21from myapp.utils import format_date

info

Set known-first-party in [tool.ruff.lint.isort] so ruff correctly identifies your own packages when sorting imports.
Type Checking — mypy & pyright

Static type checkers analyze your code without running it, catching type errors before they reach production. mypy is the original and most widely adopted. pyright (by Microsoft) is faster and stricter, and powers the Pylance extension in VS Code.

Featuremypypyright
SpeedSlower (Python)Faster (TypeScript)
StrictnessConfigurableStricter by default
Stdlib CoverageExcellent (bundled stubs)Excellent (bundled stubs)
IDE SupportGood (LS available)Excellent (Pylance)
pyrightconfigN/Apyrightconfig.json
untitled.bash
Bash
1# Install mypy
2pip install mypy
3
4# Check the project
5mypy src/
6
7# Strict mode
8mypy --strict src/
9
10# Install pyright
11pip install pyright
12
13# Check with pyright
14pyright src/
pyproject.toml
TOML
1# pyproject.toml — mypy configuration
2[tool.mypy]
3python_version = "3.12"
4strict = true
5warn_return_any = true
6warn_unused_configs = true
7disallow_untyped_defs = true
8check_untyped_defs = true
9no_implicit_optional = true
10
11[[tool.mypy.overrides]]
12module = "tests.*"
13disallow_untyped_defs = false
14
15[[tool.mypy.overrides]]
16module = "third_party_lib.*"
17ignore_missing_imports = true
type_checking.py
Python
1# Type errors caught by mypy/pyright
2
3def greet(name: str) -> str:
4 return f"Hello, {name}"
5
6greet(42) # Error: int is not str
7greet(None) # Error: None is not str
8
9# Advanced: Protocol, TypeVar, Generic
10from typing import Protocol, TypeVar
11
12T = TypeVar("T")
13
14class Comparable(Protocol):
15 def __lt__(self, other: "Comparable") -> bool: ...
16
17def max_item(items: list[T]) -> T:
18 """Find the maximum item in a list."""
19 result = items[0]
20 for item in items[1:]:
21 if item > result:
22 result = item
23 return result
📝

note

Use mypy for CI and pyright (via Pylance) in your editor for the best of both worlds — mypy for comprehensive checks, pyright for fast in-editor feedback.
Pre-commit Hooks

pre-commit runs linters and formatters automatically on every git commit. This prevents broken code from ever reaching your repository. Hooks are defined in .pre-commit-config.yaml and installed once per developer clone.

untitled.bash
Bash
1# Install pre-commit
2pip install pre-commit
3
4# Install hooks in the repo
5pre-commit install
6
7# Run against all files
8pre-commit run --all-files
9
10# Update hook versions
11pre-commit autoupdate
.pre-commit-config.yaml
YAML
1# .pre-commit-config.yaml
2repos:
3 - repo: https://github.com/astral-sh/ruff-pre-commit
4 rev: v0.8.0
5 hooks:
6 - id: ruff
7 args: [--fix]
8 - id: ruff-format
9
10 - repo: https://github.com/pre-commit/mirrors-mypy
11 rev: v1.13.0
12 hooks:
13 - id: mypy
14 additional_dependencies: [types-requests]
15
16 - repo: https://github.com/pre-commit/pre-commit-hooks
17 rev: v5.0.0
18 hooks:
19 - id: trailing-whitespace
20 - id: end-of-file-fixer
21 - id: check-yaml
22 - id: check-toml
23 - id: check-added-large-files
24 args: [--maxkb=500]

warning

Pre-commit hooks are only as good as the developer having them installed. Always enforce the same checks in CI as a safety net — pre-commit is a convenience, not a guarantee.
Editor Integration

Configure your editor to lint and format on save for instant feedback. This eliminates the need to remember CLI commands and catches issues as you type.

VS Code

Install the Ruff extension (charliermarsh.ruff) and Pylance (ms-python.vscode-pylance). Add to your .vscode/settings.json:

.vscode/settings.json
JSON
1{
2 "python.defaultInterpreterPath": ".venv/bin/python",
3 "[python]": {
4 "editor.defaultFormatter": "charliermarsh.ruff",
5 "editor.formatOnSave": true,
6 "editor.codeActionsOnSave": {
7 "source.fixAll.ruff": "explicit",
8 "source.organizeImports.ruff": "explicit"
9 }
10 },
11 "ruff.lint.args": ["--config", "pyproject.toml"],
12 "pyright.typeCheckingMode": "basic",
13 "pyright.analysis.autoImportCompletions": true
14}

PyCharm

PyCharm has built-in support for black and ruff. Go to Settings → Tools → Black and enable Use black formatter. For ruff, install the Ruff plugin from the marketplace and enable it in Settings → Tools → Ruff. Configure File Watchers under Tools → File Watchers to auto-format on save.

info

For both editors, ensure the .venv is activated or the interpreter path is set correctly — otherwise formatters and type checkers will not find your project dependencies.
CI Integration

Run linting, formatting, and type checking in CI to enforce standards across all contributors. Fail the build on any violation to maintain code quality.

.github/workflows/lint.yml
YAML
1# .github/workflows/lint.yml
2name: Lint
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8
9jobs:
10 lint:
11 runs-on: ubuntu-latest
12 steps:
13 - uses: actions/checkout@v4
14
15 - uses: actions/setup-python@v5
16 with:
17 python-version: "3.12"
18
19 - name: Install dependencies
20 run: |
21 pip install ruff mypy
22 pip install -e ".[dev]"
23
24 - name: Ruff lint
25 run: ruff check .
26
27 - name: Ruff format check
28 run: ruff format --check .
29
30 - name: Mypy
31 run: mypy src/
untitled.bash
Bash
1# Quick CI commands (any CI system)
2
3# Lint with ruff
4ruff check .
5
6# Format check (non-destructive)
7ruff format --check .
8
9# Type check
10mypy src/
11
12# All-in-one make target
13make lint: # from Makefile
14 ruff check . && ruff format --check . && mypy src/

best practice

Add ruff format --check . (not ruff format .) in CI — you want to detect unformatted code, not silently fix it. Developers should format locally before pushing.

Recommended pyproject.toml Config

Here is a consolidated pyproject.toml configuration combining all tools discussed in this guide:

pyproject.toml
TOML
1# pyproject.toml — Full linting & formatting config
2[tool.ruff]
3target-version = "py312"
4line-length = 88
5
6[tool.ruff.lint]
7select = [
8 "E", "W", "F", "I", "N", "UP", "B",
9 "A", "C4", "SIM", "TCH", "RUF",
10]
11ignore = ["E501"]
12
13[tool.ruff.lint.per-file-ignores]
14"tests/**/*.py" = ["S101"]
15"__init__.py" = ["F401"]
16
17[tool.ruff.lint.isort]
18known-first-party = ["myapp"]
19
20[tool.ruff.format]
21quote-style = "double"
22indent-style = "space"
23
24[tool.mypy]
25python_version = "3.12"
26strict = true
27warn_return_any = true
28warn_unused_configs = true
29disallow_untyped_defs = true
30
31[[tool.mypy.overrides]]
32module = "tests.*"
33disallow_untyped_defs = false
34
35[[tool.mypy.overrides]]
36module = "third_party_lib.*"
37ignore_missing_imports = true
$Blueprint — Engineering Documentation·Section ID: PYTHON-LINT·Revision: 1.0