Architecture

Typer callback functions explained

Choose frameworks, structure command trees, and design extensible, testable command-line systems that can scale with your product or team.

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 | None for 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 contextvars for shared state
  • Combine with typer.Option(is_eager=True) for early validation
  • Avoid blocking I/O; keep callbacks lightweight