Skip to main content

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

ToolPurposeInstall
ruffLinting + formatting (replaces flake8, isort, black)pip install ruff
mypyStatic type checkingpip install mypy
blackCode formatting onlypip install black
pre-commitRun checks before every git commitpip 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
# 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

MistakeFix
Not running tools before committingInstall pre-commit hooks
Ignoring all ruff rulesFix the errors — they're real issues
mypy --strict on legacy codeStart with --ignore-missing-imports
No CI quality gateAdd ruff/mypy/pytest to CI
Different configs per developerCommit 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

What's Next

Module 8: Environments and Packaging