Static Analysis and Formatting
What You'll Learn
How to set up and use the key Python code quality tools — what each one does, how to configure them, and how to integrate them into your workflow.
The Quality Toolchain
| Tool | Purpose | Install |
|---|---|---|
| ruff | Linting + formatting (replaces flake8, isort, black) | pip install ruff |
| mypy | Static type checking | pip install mypy |
| black | Code formatting only | pip install black |
| pre-commit | Run checks before every git commit | pip install pre-commit |
For new projects, use ruff (it does linting + formatting, is very fast, and replaces multiple tools).
ruff — Linting
ruff checks your code for problems without running it:
# Check a directory
ruff check src/
# Check and auto-fix what it can
ruff check src/ --fix
# Check a single file
ruff check src/main.py
Example output:
src/main.py:5:1: F401 `os` imported but unused
src/main.py:12:9: E711 Comparison to `None` (use `is` or `is not`)
src/utils.py:3:1: I001 Import block is unsorted and/or formatted incorrectly
What ruff catches:
- Unused imports (
F401) - Undefined names (
F821) - Style violations (
E,W) - Import ordering (
I) - Security issues (
S) - Type annotation issues (
ANN) - And hundreds more rules
ruff — Formatting
ruff format src/ # format all Python files
ruff format src/main.py # format one file
ruff format --check src/ # check without changing (for CI)
Before:
x=1
y = 2
z= x+y
print( x,y,z )
def foo( a,b ):
return a+b
After:
x = 1
y = 2
z = x + y
print(x, y, z)
def foo(a, b):
return a + b
mypy — Type Checking
mypy finds type errors before they crash at runtime:
mypy src/
mypy src/main.py --strict
Example: catching a bug before it runs:
# calculator.py
def multiply(a: int, b: int) -> int:
return a * b
result = multiply("3", 4) # wrong type!
calculator.py:4: error: Argument 1 to "multiply" has incompatible type "str"; expected "int"
Common mypy Options
mypy src/ # check directory
mypy --ignore-missing-imports src/ # skip untyped 3rd-party libs
mypy --strict src/ # strictest checking
Configuring Tools with pyproject.toml
Keep all tool config in one file:
# pyproject.toml
[tool.ruff]
line-length = 99
target-version = "py311"
src = ["src"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by formatter)
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # allow assert in tests
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
pre-commit — Automatic Checks Before Every Commit
pre-commit runs your quality tools automatically before git commit. Bad commits never get through:
pip install pre-commit
Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
additional_dependencies: [types-requests]
Install the hooks:
pre-commit install # installs hooks into .git/hooks/
pre-commit run --all-files # run manually on everything
Now every git commit automatically runs ruff and mypy. If they fail, the commit is rejected until you fix the issues.
CI/CD Integration
Add quality checks to your GitHub Actions workflow:
# .github/workflows/quality.yml
name: Code Quality
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install ruff mypy pytest
- run: ruff check src/
- run: ruff format --check src/
- run: mypy src/
- run: pytest tests/ -v
Recommended Workflow
# Before committing
ruff check src/ --fix # fix auto-fixable issues
ruff format src/ # format code
mypy src/ # type check
pytest tests/ # run tests
# Or with pre-commit installed: just commit
git add -A
git commit -m "feature: add user validation"
# pre-commit runs automatically
Common Mistakes
| Mistake | Fix |
|---|---|
| Not running tools before committing | Install pre-commit hooks |
| Ignoring all ruff rules | Fix the errors — they're real issues |
mypy --strict on legacy code | Start with --ignore-missing-imports |
| No CI quality gate | Add ruff/mypy/pytest to CI |
| Different configs per developer | Commit pyproject.toml to the repo |
Quick Reference
# ruff
ruff check src/ # lint
ruff check src/ --fix # lint + auto-fix
ruff format src/ # format
ruff format --check src/ # check formatting (CI)
# mypy
mypy src/
mypy --ignore-missing-imports src/
# pre-commit
pre-commit install
pre-commit run --all-files
# pytest with coverage
pytest tests/ --cov=src --cov-report=term-missing
# pyproject.toml
[tool.ruff]
line-length = 99
[tool.ruff.lint]
select = ["E", "F", "I", "N"]
[tool.mypy]
ignore_missing_imports = true