Typer callback functions explained
Environment Setup
Initialize your project with modern dependency management. Ensure Python 3.10+ is active.
# pyproject.toml
[project]
name = "typer-callbacks-demo"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["typer>=0.9.0"]
Install and verify the runtime:
pip install -e .
python -c "import typer; print(typer.__version__)"
What Are Typer Callback Functions?
Typer callbacks are execution hooks that intercept parsed CLI arguments before command dispatch. They enable cross-command validation, environment setup, and dynamic option modification. When designing Modern Python CLI Frameworks & Architecture, callbacks replace manual if/else routing with declarative, type-safe preprocessing.
Key operational characteristics:
- Execute synchronously before command logic runs
- Preserve Python 3.10+ type hints via
typer.Callback() - Run in exact definition order across the CLI tree
Exact Error & Minimal Fix
The most frequent failure is TypeError: callback() takes 1 positional argument but 2 were given. Typer automatically passes the parsed value to the callback, but mismatched function signatures trigger this crash. The resolution requires a single-parameter signature matching the option's type.
import typer
# BROKEN: Accepts ctx, but Typer only passes the value
def validate_version(ctx, value: str):
return value
# FIXED: Single parameter, explicit type hint
def validate_version_fixed(value: str | None) -> str | None:
if value and not value.startswith('v'):
raise typer.BadParameter('Version must start with "v"')
return value
app = typer.Typer()
@app.command()
def deploy(version: str | None = typer.Option(None, callback=validate_version_fixed)):
typer.echo(f'Deploying {version}')
if __name__ == '__main__':
app()
Implementation rules:
- Always accept exactly one argument
- Return the processed value or raise
typer.BadParameter - Use
str | Nonefor optional options
Test the validation directly from your terminal:
python cli.py deploy --version 1.0.0 # Fails with BadParameter
python cli.py deploy --version v1.0.0 # Succeeds
Production-Ready Global State Pattern
Callbacks excel at initializing shared resources like database connections or config parsers. By attaching a callback to a hidden or global flag, you can hydrate context objects before any subcommand executes. For teams evaluating framework trade-offs, reviewing Typer vs Click: When to Use Each clarifies when Typer's dependency injection outperforms raw Click context passing.
import typer
from pathlib import Path
config_path: Path | None = None
def load_config(value: str | None) -> str | None:
global config_path
if value:
config_path = Path(value)
if not config_path.exists():
raise typer.BadParameter('Config file not found')
return value
app = typer.Typer()
@app.command()
def run(config: str | None = typer.Option(None, '--config', callback=load_config)):
if config_path:
typer.echo(f'Loaded: {config_path}')
else:
typer.echo('Using defaults')
if __name__ == '__main__':
app()
Best practices for state management:
- Use module-level or
contextvarsfor shared state - Combine with
typer.Option(is_eager=True)for early validation - Avoid blocking I/O; keep callbacks lightweight