Migrating from Typer
August 28, 2026 ยท View on GitHub
When this path fits
Use this path when typed Typer commands and dependency injection are already valuable. Install the optional adapter so Typer remains optional for Click-only consumers:
python -m pip install 'base-cli[typer]'
Incremental change
Before, Typer owns the process entry point:
import typer
app = typer.Typer()
@app.command()
def status(verbose: bool = typer.Option(False, "--verbose")) -> None:
typer.echo("ready" if not verbose else "ready (verbose)")
if __name__ == "__main__":
app()
After, retain the Typer declarations and attach the materialized command:
import base_cli
import typer
app = typer.Typer()
@app.command()
def status(verbose: bool = typer.Option(False, "--verbose")) -> None:
context = base_cli.get_current_context()
context.log.info("status requested")
typer.echo("ready" if not verbose else "ready (verbose)")
command = base_cli.attach_typer(app, name="example")
if __name__ == "__main__":
raise SystemExit(base_cli.run_app(command))
Typer remains responsible for decorators, type-driven parameters, nested apps,
help, completion, dependency injection, and Typer/Click exceptions. The
adapter returns Typer's generated Click command; base-cli owns the lifecycle,
context, logging, redaction, runtime state, cleanup, history hooks, and output
contracts. For custom configuration use base_cli.attach_typer(app, app=base_cli.App(...)).
Verification and rollback
- Test the supported Typer and Python matrix, including the vendored Click boundary used by newer Typer releases.
- Compare typed defaults, callback injection, help, completion, and exit codes with the original app.
- Add contract fixtures and exercise debug, quiet, JSON, and retained diagnostics.
- Keep the old Typer entry point available until the wheel-first pilot is green; restore it to roll back without rewriting command code.
See typer-adapter.md for adapter details.