Python — Linting & Formatting
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.
| Tool | Purpose | Replaces |
|---|---|---|
| ruff | Lint + Format | flake8, isort, pycodestyle, pyflakes, bandit |
| black | Code formatting | autopep8, yapf |
| mypy | Static type checking | — |
| pyright | Static type checking | mypy (faster, stricter) |
| pre-commit | Git hook automation | husky, lint-staged (JS) |
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.
| Rule | Convention |
|---|---|
| Indentation | 4 spaces (no tabs) |
| Line Length | 79 chars (code), 72 chars (docstrings) |
| Naming | snake_case functions/vars, PascalCase classes, UPPER_SNAKE constants |
| Imports | One per line, grouped (stdlib, third-party, local) |
| Blanks | 2 lines before top-level, 1 between methods |
| Quotes | Single or double (be consistent) |
| 1 | # ✅ Good PEP 8 style |
| 2 | from pathlib import Path |
| 3 | |
| 4 | MAX_RETRIES = 3 |
| 5 | |
| 6 | |
| 7 | def 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 | |
| 13 | class 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 |
| 25 | from os import*;from pathlib import * |
| 26 | MAX_retries=3 |
| 27 | def Calculate_Total(items,tax_rate=.08): |
| 28 | subtotal=sum( [i['price']*i['quantity'] for i in items]) |
| 29 | return subtotal*(1+ tax_rate) |
| 30 | class user_profile: |
| 31 | def __init__(self,name,email): |
| 32 | self.name=name |
| 33 | self.email=email |
info
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.
| 1 | # Install ruff |
| 2 | pip install ruff |
| 3 | |
| 4 | # Lint the project |
| 5 | ruff check . |
| 6 | |
| 7 | # Lint and auto-fix |
| 8 | ruff check --fix . |
| 9 | |
| 10 | # Format the project |
| 11 | ruff format . |
| 12 | |
| 13 | # Format check only (CI) |
| 14 | ruff format --check . |
Configure ruff entirely in pyproject.toml. The [tool.ruff] section controls line length, target Python version, rule selection, and per-file overrides.
| 1 | [tool.ruff] |
| 2 | target-version = "py312" |
| 3 | line-length = 88 # Black-compatible default |
| 4 | |
| 5 | [tool.ruff.lint] |
| 6 | select = [ |
| 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 | ] |
| 20 | ignore = [ |
| 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] |
| 29 | known-first-party = ["myapp"] |
| 30 | |
| 31 | [tool.ruff.format] |
| 32 | quote-style = "double" |
| 33 | indent-style = "space" |
| 34 | line-ending = "auto" |
pro tip
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.
| 1 | # Install |
| 2 | pip install black |
| 3 | |
| 4 | # Format all files |
| 5 | black . |
| 6 | |
| 7 | # Check without modifying |
| 8 | black --check . |
| 9 | |
| 10 | # Format a single file |
| 11 | black src/my_module.py |
| 12 | |
| 13 | # Show diff without applying |
| 14 | black --diff . |
| 1 | # pyproject.toml — Black config (skip if using ruff format) |
| 2 | [tool.black] |
| 3 | line-length = 88 |
| 4 | target-version = ["py312"] |
| 5 | include = "\.pyi?$" |
| 6 | exclude = ''' |
| 7 | /( |
| 8 | \bz\b |
| 9 | | \bbuild\b |
| 10 | | \bdist\b |
| 11 | | \bnode_modules\b |
| 12 | | \b\.eggs\b |
| 13 | )/ |
| 14 | ''' |
note
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.
| 1 | # Using ruff (recommended) |
| 2 | ruff check --select I --fix . |
| 3 | |
| 4 | # Using standalone isort |
| 5 | pip install isort |
| 6 | isort . |
| 7 | isort --check-only . # CI mode |
| 1 | # Before isort |
| 2 | import json |
| 3 | from pathlib import Path |
| 4 | import os |
| 5 | from myapp.models import User |
| 6 | import sys |
| 7 | from myapp.utils import format_date |
| 8 | from collections import OrderedDict |
| 9 | import requests |
| 10 | |
| 11 | # After isort (stdlib → third-party → local) |
| 12 | import json |
| 13 | import os |
| 14 | import sys |
| 15 | from collections import OrderedDict |
| 16 | from pathlib import Path |
| 17 | |
| 18 | import requests |
| 19 | |
| 20 | from myapp.models import User |
| 21 | from myapp.utils import format_date |
info
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.
| Feature | mypy | pyright |
|---|---|---|
| Speed | Slower (Python) | Faster (TypeScript) |
| Strictness | Configurable | Stricter by default |
| Stdlib Coverage | Excellent (bundled stubs) | Excellent (bundled stubs) |
| IDE Support | Good (LS available) | Excellent (Pylance) |
| pyrightconfig | N/A | pyrightconfig.json |
| 1 | # Install mypy |
| 2 | pip install mypy |
| 3 | |
| 4 | # Check the project |
| 5 | mypy src/ |
| 6 | |
| 7 | # Strict mode |
| 8 | mypy --strict src/ |
| 9 | |
| 10 | # Install pyright |
| 11 | pip install pyright |
| 12 | |
| 13 | # Check with pyright |
| 14 | pyright src/ |
| 1 | # pyproject.toml — mypy configuration |
| 2 | [tool.mypy] |
| 3 | python_version = "3.12" |
| 4 | strict = true |
| 5 | warn_return_any = true |
| 6 | warn_unused_configs = true |
| 7 | disallow_untyped_defs = true |
| 8 | check_untyped_defs = true |
| 9 | no_implicit_optional = true |
| 10 | |
| 11 | [[tool.mypy.overrides]] |
| 12 | module = "tests.*" |
| 13 | disallow_untyped_defs = false |
| 14 | |
| 15 | [[tool.mypy.overrides]] |
| 16 | module = "third_party_lib.*" |
| 17 | ignore_missing_imports = true |
| 1 | # Type errors caught by mypy/pyright |
| 2 | |
| 3 | def greet(name: str) -> str: |
| 4 | return f"Hello, {name}" |
| 5 | |
| 6 | greet(42) # Error: int is not str |
| 7 | greet(None) # Error: None is not str |
| 8 | |
| 9 | # Advanced: Protocol, TypeVar, Generic |
| 10 | from typing import Protocol, TypeVar |
| 11 | |
| 12 | T = TypeVar("T") |
| 13 | |
| 14 | class Comparable(Protocol): |
| 15 | def __lt__(self, other: "Comparable") -> bool: ... |
| 16 | |
| 17 | def 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
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.
| 1 | # Install pre-commit |
| 2 | pip install pre-commit |
| 3 | |
| 4 | # Install hooks in the repo |
| 5 | pre-commit install |
| 6 | |
| 7 | # Run against all files |
| 8 | pre-commit run --all-files |
| 9 | |
| 10 | # Update hook versions |
| 11 | pre-commit autoupdate |
| 1 | # .pre-commit-config.yaml |
| 2 | repos: |
| 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
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:
| 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
Run linting, formatting, and type checking in CI to enforce standards across all contributors. Fail the build on any violation to maintain code quality.
| 1 | # .github/workflows/lint.yml |
| 2 | name: Lint |
| 3 | |
| 4 | on: |
| 5 | push: |
| 6 | branches: [main] |
| 7 | pull_request: |
| 8 | |
| 9 | jobs: |
| 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/ |
| 1 | # Quick CI commands (any CI system) |
| 2 | |
| 3 | # Lint with ruff |
| 4 | ruff check . |
| 5 | |
| 6 | # Format check (non-destructive) |
| 7 | ruff format --check . |
| 8 | |
| 9 | # Type check |
| 10 | mypy src/ |
| 11 | |
| 12 | # All-in-one make target |
| 13 | make lint: # from Makefile |
| 14 | ruff check . && ruff format --check . && mypy src/ |
best practice
Recommended pyproject.toml Config
Here is a consolidated pyproject.toml configuration combining all tools discussed in this guide:
| 1 | # pyproject.toml — Full linting & formatting config |
| 2 | [tool.ruff] |
| 3 | target-version = "py312" |
| 4 | line-length = 88 |
| 5 | |
| 6 | [tool.ruff.lint] |
| 7 | select = [ |
| 8 | "E", "W", "F", "I", "N", "UP", "B", |
| 9 | "A", "C4", "SIM", "TCH", "RUF", |
| 10 | ] |
| 11 | ignore = ["E501"] |
| 12 | |
| 13 | [tool.ruff.lint.per-file-ignores] |
| 14 | "tests/**/*.py" = ["S101"] |
| 15 | "__init__.py" = ["F401"] |
| 16 | |
| 17 | [tool.ruff.lint.isort] |
| 18 | known-first-party = ["myapp"] |
| 19 | |
| 20 | [tool.ruff.format] |
| 21 | quote-style = "double" |
| 22 | indent-style = "space" |
| 23 | |
| 24 | [tool.mypy] |
| 25 | python_version = "3.12" |
| 26 | strict = true |
| 27 | warn_return_any = true |
| 28 | warn_unused_configs = true |
| 29 | disallow_untyped_defs = true |
| 30 | |
| 31 | [[tool.mypy.overrides]] |
| 32 | module = "tests.*" |
| 33 | disallow_untyped_defs = false |
| 34 | |
| 35 | [[tool.mypy.overrides]] |
| 36 | module = "third_party_lib.*" |
| 37 | ignore_missing_imports = true |