.agents/skills/dignified-python/references/module-design.md
Read when: Creating new modules, adding module-level code, using @cache decorator
Avoid computation and side effects at import time. Defer to function calls.
Module-level code runs when the module is imported. Side effects at import time cause:
# WRONG: Path computed at import time
SESSION_ID_FILE = Path(".app/scratch/current-session-id")
def get_session_id() -> str | None:
if SESSION_ID_FILE.exists():
return SESSION_ID_FILE.read_text(encoding="utf-8")
return None
# WRONG: Config loaded at import time
CONFIG = load_config() # I/O at import!
# WRONG: Connection established at import time
DB_CLIENT = DatabaseClient(os.environ["DB_URL"]) # Side effect at import!
Use @cache for deferred computation:
from functools import cache
# CORRECT: Defer computation until first call
@cache
def _session_id_file_path() -> Path:
"""Return path to session ID file (cached after first call)."""
return Path(".app/scratch/current-session-id")
def get_session_id() -> str | None:
session_file = _session_id_file_path()
if session_file.exists():
return session_file.read_text(encoding="utf-8")
return None
Use functions for resources:
# CORRECT: Defer resource creation to function call
@cache
def get_config() -> Config:
"""Load config on first call, cache result."""
return load_config()
@cache
def get_db_client() -> DatabaseClient:
"""Create database client on first call."""
return DatabaseClient(os.environ["DB_URL"])
Simple, static values that don't involve computation or I/O:
# ACCEPTABLE: Static constants
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
SUPPORTED_FORMATS = frozenset({"json", "yaml", "toml"})
# commands/sync.py
def register_commands(cli_group):
"""Register commands with CLI group (avoids circular import)."""
from myapp.cli import sync_command # Breaks circular dependency
cli_group.add_command(sync_command)
When to use:
def process_data(data: dict, dry_run: bool = False) -> None:
if dry_run:
# Inline import: Only needed for dry-run mode
from myapp.dry_run import NoopProcessor
processor = NoopProcessor()
else:
processor = RealProcessor()
processor.execute(data)
When to use:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from myapp.models import User # Only for type hints
def process_user(user: "User") -> None:
...
When to use:
Some packages have genuinely heavy import costs (pyspark, jupyter ecosystem, large ML frameworks). Deferring these imports can improve CLI startup time.
However, apply "innocent until proven guilty":
# ACCEPTABLE: Measured heavy import (adds 800ms to startup)
def run_spark_job(config: SparkConfig) -> None:
from pyspark.sql import SparkSession # Heavy: 800ms import time
session = SparkSession.builder.getOrCreate()
...
# WRONG: Speculative deferral without measurement
def check_staleness(project_dir: Path) -> None:
# Inline imports to avoid import-time side effects <- WRONG: no evidence
from myapp.staleness import get_version
...
When NOT to defer:
Before writing module-level code:
Path() construction)?If any answer is "yes", wrap in a @cache-decorated function instead.
Before inline imports:
Default: Module-level imports