Skip to content

Python Style Guide (Ruff)

All Python code must pass Ruff linting. The Ruff configuration is defined in the root pyproject.toml. This guide is the expanded reference for the summary in AGENTS.md.

  • Line length: 88 characters (hard limit)
  • Target Python version: 3.11+
  • Enabled rule sets: E4, E7, E9, F (Pyflakes), B (flake8-bugbear), I (isort), UP (pyupgrade), C4 (comprehensions), SIM (simplify), RUF (Ruff-specific)
  • Ignored rules: E501 (line length handled by formatter), F401 (unused imports for re-exports), F403 (star imports in __init__.py), B008 (FastAPI Depends pattern), B904 (FastAPI HTTPException), SIM117 (nested with statements), RUF001/RUF003 (intentional unicode)

Group imports: standard library β†’ third-party β†’ local. Use absolute imports, not relative (unless in the same package). Sort alphabetically within each group.

# βœ… Correct
import os
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
from app.models import Order
from app.services import OrderService
# ❌ Wrong β€” mixed groups, relative imports
from .models import Order
import os
from fastapi import FastAPI

Break long lines using parentheses for function calls; use trailing commas in multi-line structures.

# βœ… Correct
result = some_function(
arg1,
arg2,
arg3,
)
# ❌ Wrong β€” line too long
result = some_function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)

Prefer list/dict/set comprehensions over loops when appropriate. Don’t use comprehensions for side effects.

# βœ… Correct
squares = [x**2 for x in range(10)]
result = {k: v for k, v in items.items() if v > 0}
# ❌ Wrong β€” comprehension for side effects
[print(x) for x in items] # Use a loop instead

Use if x: instead of if x is True:; combine multiple if statements when possible.

# βœ… Correct
if x and y:
do_something()
# ❌ Wrong
if x is True:
if y is True:
do_something()

Never use undefined variables. Remove unused imports (except in __init__.py for re-exports) and unused variables (except in tests where mocks are used).

Don’t use mutable defaults β€” use None instead. Don’t call functions in default arguments (except FastAPI Depends).

# βœ… Correct
def process(items: list[str] | None = None) -> list[str]:
if items is None:
items = []
return items
# ❌ Wrong β€” mutable default
def process(items: list[str] = []):
return items

Use modern Python 3.11+ syntax: list[str] instead of List[str], dict[str, int] instead of Dict[str, int], f-strings instead of .format() or %.

# βœ… Correct (Python 3.11+)
def process(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# ❌ Wrong β€” old syntax
from typing import List, Dict
def process(items: List[str]) -> Dict[str, int]:
...

8. Error handling (E7 rules β€” bare except: is E722)

Section titled β€œ8. Error handling (E7 rules β€” bare except: is E722)”

Use specific exception types, not bare except:.

# βœ… Correct
try:
result = risky_operation()
except ValueError as e:
logger.error(f"Value error: {e}")
raise
# ❌ Wrong
try:
result = risky_operation()
except: # Bare except
pass
File pattern Ignored rules Why
**/main.py B008, B904, SIM117, E402 FastAPI patterns (Depends(), lazy imports)
**/tasks.py SIM102, SIM105, SIM117, E731 Worker task patterns
**/init_langfuse_prompts.py RUF001, UP035 Intentional unicode, older-Python compatibility
**/tests/** SIM117, F841, E402 Test patterns (mocks, grouping)
**/conftest.py E402 Imports after test setup
**/__init__.py F401 Re-exports
data-engineering/workbench/notebook_template.py B018 Marimo cell-output idiom (data-engineering/workbench-repo is in extend-exclude)
**/crud.py SIM105, B904, UP035 Shared library patterns (explicit pass, custom error wrapping)
**/config.py B904 raise from within except (custom error wrapping)
**/extractors.py E402 Module imports after env var setup (GEMINI_API_KEY before mirascope)
**/r2.py B904 raise from within except (custom R2Error wrapping with metadata)
**/utils/**/*.py UP035 typing.Dict/List backward compatibility
**/observability/**/*.py RUF013 Implicit Optional in metrics function signatures
**/pdf_performance_metrics.py B025 Duplicate exception (layered import fallbacks)

Note: Customer Marimo notebooks live in the data-engineering/workbench-repo submodule, which is excluded via extend-exclude in root pyproject.toml. See pyproject.toml ([tool.ruff.lint.per-file-ignores]) for the authoritative list and per-rule rationale comments.

FastAPI-specific patterns (allowed in main.py and routes)

Section titled β€œFastAPI-specific patterns (allowed in main.py and routes)”
# βœ… Correct β€” B008 is ignored in main.py
from fastapi import FastAPI, Depends
def get_db():
return db_connection()
@app.get("/items")
async def get_items(db=Depends(get_db)): # Function call in default OK
return db.query(Item).all()
# βœ… Correct β€” F841 is ignored in tests
def test_something():
mock_db = Mock() # Unused variable OK in tests
result = function_under_test()
assert result == expected
  • Before: check if the file matches a per-file-ignore pattern; use modern type hints; keep lines under 88 chars; organize imports correctly.
  • After: verify with ruff check <file> or pnpm lint:python; auto-fix with ruff check --fix <file> or pnpm lint:python:fix. For the Dagster app specifically: cd apps/dagster && ruff check .

Remember: when in doubt, run ruff check <file> to see what violations exist, then fix them before committing.