Conventions
Project coding conventions for all Python code. Covers structure, style, quality, and logging. For detailed tool configurations see tooling.md. For file templates see templates.md.
General Principles
- Prioritize readability over cleverness
- Follow principle of least surprise
- Keep functions small and focused (single responsibility)
- Profile before optimizing — never guess at bottlenecks
- Write tests before or alongside implementation
Project Structure
Use src-layout with layered architecture:
${PROJECT_NAME}/
├── pyproject.toml
├── mkdocs.yml
├── docs/
│ ├── index.md
│ └── scripts/
│ └── gen_ref_pages.py
├── src/
│ └── ${PROJECT_NAME}/
│ ├── __init__.py
│ ├── _version.py # Auto-generated by hatch-vcs
│ ├── contracts/ # API schemas — single source of truth
│ ├── core/ # Infrastructure only (logger, config, exceptions)
│ ├── models/ # Shared domain models (Pydantic)
│ ├── services/ # Business logic (framework-agnostic)
│ └── interfaces/ # Entry points
│ ├── cli/ # CLI (Typer)
│ ├── rest/ # REST API (FastAPI)
│ └── rpc/ # JSON-RPC over WebSocket
└── tests/
├── conftest.py
└── ... # Mirror src/ structure
Layer Rules
- contracts/: API schemas (OpenAPI, OpenRPC) — single source of truth, written first
- core/: Infrastructure only — NO business logic (logger, config, exceptions, utils)
- models/: Pydantic models matching contract schemas — written before implementation
- services/: Business logic — framework-agnostic, no Typer/FastAPI imports, fully testable
- interfaces/: Thin wrappers — call services, handle I/O, minimal logic
Python Version
Target Python 3.13+:
[project]
requires-python = ">=3.13"
Imports
- Standard library
- Third-party packages
- Local imports
import os
from pathlib import Path
import httpx
from pydantic import BaseModel
from ${PROJECT_NAME}.core import logger
from ${PROJECT_NAME}.services.processor import process_data
- Prefer
from x import yfor specific items - Use absolute imports, not relative
Type Hints
Always use type hints on all function signatures:
def process_file(
filepath: Path,
options: dict[str, Any] | None = None,
) -> ProcessResult:
...
Modern Typing (Python 3.13+)
list[str] # not List[str]
dict[str, int] # not Dict[str, int]
str | None # not Optional[str]
int | str # not Union[int, str]
Docstrings
Google-style for all public functions:
def fetch_data(url: str, timeout: int = 30) -> dict[str, Any]:
"""Fetch data from a URL and return parsed JSON.
Args:
url: The URL to fetch data from.
timeout: Request timeout in seconds. Defaults to 30.
Returns:
Parsed JSON response as a dictionary.
Raises:
httpx.HTTPError: If the request fails.
"""
...
Naming
- Variables: explicit names (
file_countnotfc,user_datanotud) - Constants:
ALL_CAPSat module level — extract magic numbers - Functions/methods:
snake_case, imperative verbs
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
SUPPORTED_FORMATS = {"json", "yaml", "toml"}
Logging
Never use print() — always use logger.
from ${PROJECT_NAME}.core import logger
logger.debug(f"Processing item {i} of {total}")
logger.info(f"Processing {count} files")
logger.success(f"Completed: {result}")
logger.warning(f"Rate limit approaching")
logger.error(f"Failed to process: {error}")
logger.critical("System failure")
Key Patterns
# Log exceptions with context
try:
process()
except Exception as e:
logger.error(f"Processing failed: {e}", exc_info=True)
raise
# Progress logging for long operations
for i, item in enumerate(items, 1):
process_item(item)
if i % 100 == 0:
logger.info(f"Progress: {i}/{total} items processed")
logger.success(f"Completed processing {total} items")
Testing with Logging
def test_function_logs_correctly(caplog):
with caplog.at_level("INFO"):
my_function()
assert "Expected message" in caplog.text
Error Handling
# Specific exceptions, not bare except
try:
result = process(data)
except FileNotFoundError:
logger.error(f"File not found: {filepath}")
raise
except ValueError as e:
logger.error(f"Invalid data: {e}")
raise
# Custom exceptions in core/exceptions.py
class ProcessingError(Exception):
"""Raised when processing fails."""
pass
# Validate early, fail fast
def process(data: str) -> Result:
if not data:
raise ValueError("data cannot be empty")
...
Configuration
Use environment variables via pydantic-settings:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
debug: bool = False
api_key: str
database_url: str
model_config = {"env_prefix": "${PROJECT_NAME}_"}
settings = Settings()
Refactoring
- Look for duplication, long functions, unclear names
- Extract common functionality to reusable functions
- Keep refactoring commits separate from feature commits
Code Review Checklist
Before committing:
- Type hints complete and accurate
- Google-style docstrings on public APIs
- Logger used, not
print() - Magic numbers extracted to constants
- Specific exception handling (no bare
except) - Tests comprehensive and passing
- No sensitive info in logs or code
-
uv run ruff check .passes -
uv run pyrightpasses
Quick Reference
| Aspect | Convention |
|---|---|
| Python version | 3.13+ |
| Package manager | uv |
| Build system | Hatchling + hatch-vcs |
| Versioning | Git tags (vX.Y.Z) |
| Linter/Formatter | Ruff |
| Type checker | Pyright (strict) |
| Test framework | pytest |
| Documentation | Material for MkDocs |
| Docstring style | |
| Logging | loguru (never print()) |
| CLI | Typer |
| REST API | FastAPI |
| JSON-RPC / WebSocket | jsonrpc-websocket, websockets |
| Line length | 88 |
| Import style | Absolute, sorted |
See Also
- spec-driven — contracts-first development and models placement
- project-name — resolves
${PROJECT_NAME} - documentation — MkDocs setup and API doc generation
- tdd — test-driven development with pytest