alpha

Python

Quick reference for Python patterns and tools

#python#cli#typer#rich#progress#joblib#caching#memoization

Building CLI Tools with Typer

Typer is the default choice for CLI tools. Built on Click, but with type hints instead of decorators for arguments.

uv add typer
import typer

app = typer.Typer(help="My CLI tool")

@app.command()
def greet(
    name: str = typer.Argument(..., help="Name to greet"),
    loud: bool = typer.Option(False, "--loud", "-l", help="Shout it"),
):
    """Greet someone."""
    msg = f"Hello, {name}!"
    print(msg.upper() if loud else msg)

if __name__ == "__main__":
    app()

For subcommands, create separate Typer apps and register them:

# main.py
from myapp.db.cli import app as db_app
app.add_typer(db_app, name="db")

# myapp/db/cli.py
app = typer.Typer(help="Database commands")

@app.command("ping")
def ping():
    print("PONG")

Typer over argparse: less boilerplate. Typer over Click: type hints are the API.

Progress Tracking with Rich

Use rich.progress.track() for batch processing with progress bars. Simple iterator wrapper that displays progress without boilerplate. Available via Typer dependency.

from rich.progress import track

for item in track(items, description="Processing..."):
    process(item)

Replaces ad-hoc print statements:

# Bad - manual progress tracking
for i, item in enumerate(items):
    process(item)
    if (i + 1) % 10 == 0:
        print(f"Processed {i + 1}/{len(items)}...")

# Good - rich progress bar
for item in track(items, description="Processing..."):
    process(item)

For batch processing with offsets:

batches = list(range(0, len(items), batch_size))
for start in track(batches, description="Fetching..."):
    batch = items[start : start + batch_size]
    process_batch(batch)

Rich comes bundled with Typer. No additional dependency needed.

Joblib Memory for Disk-Based Caching

Use joblib.Memory for caching expensive function results to disk. Automatic invalidation based on function arguments. Cache persists across process restarts.

Cache Location

Cache directory MUST use Path for portability. Hardcoded strings break across environments.

# Bad
memory = Memory("cache")
memory = Memory("/tmp/cache")

# Good
from pathlib import Path
CACHE_DIR = Path.home() / ".cache" / "myapp"
memory = Memory(CACHE_DIR, verbose=0)

Function Arguments

All arguments to cached functions MUST be hashable. Strings, numbers, tuples work. Lists and dicts do not.

# Bad - list is not hashable
@memory.cache
def process(items: list) -> dict:
    ...

# Good - tuple is hashable
@memory.cache
def process(items: tuple) -> dict:
    ...

Joblib vs lru_cache

Use joblib.Memory when cache MUST persist across runs. Use functools.lru_cache for in-memory caching within a single process.

RequirementTool
Persist across runsjoblib.Memory
In-memory onlyfunctools.lru_cache
Large return values (pickled to disk)joblib.Memory
Fast, small resultsfunctools.lru_cache

Cache Invalidation

Clear cache explicitly when source data changes. Joblib does not detect external changes.

memory.clear()       # Clear all cached functions
fetch_data.clear()   # Clear specific function only

References