Last modified: Sep 13, 2025 By Alexander Williams

Python Typer Global Options with Root Callbacks

Python Typer makes CLI development easy. Global options with root callbacks are powerful. They let you run code before any command.

This is perfect for setup tasks. You can initialize resources. Or process shared options. All before your main command executes.

Understanding invoke_without_command

The invoke_without_command parameter is key. It controls callback behavior. When set to True, the callback runs even without subcommands.

This enables standalone mode. Your app can work with or without specific commands. It increases flexibility for users.

You might use this for version display. Or help output. Even configuration loading.

Basic Global Option Setup

Here's a simple example. We create a global verbose option.

 
import typer

app = typer.Typer()

@app.callback(invoke_without_command=True)
def main(ctx: typer.Context, verbose: bool = False):
    if verbose:
        print("Verbose mode enabled")
    if ctx.invoked_subcommand is None:
        print("No command specified")

@app.command()
def hello(name: str):
    print(f"Hello {name}!")

if __name__ == "__main__":
    app()

This code creates a callback. It accepts a verbose flag. The callback runs with or without commands.

Test it without commands:


$ python app.py --verbose
Verbose mode enabled
No command specified

Now test with a command:


$ python app.py --verbose hello World
Verbose mode enabled
Hello World!

Advanced Configuration Loading

Global options work well for configuration. You can load settings once. Then use them across all commands.

This example shows config file loading.

 
import typer
import json
from pathlib import Path

app = typer.Typer()

@app.callback(invoke_without_command=True)
def main(ctx: typer.Context, config: Path = typer.Option("config.json")):
    if config.exists():
        with open(config) as f:
            settings = json.load(f)
        ctx.obj = settings
        if ctx.invoked_subcommand is None:
            print(f"Loaded config from {config}")
    else:
        raise typer.BadParameter(f"Config file {config} not found")

@app.command()
def show_config(ctx: typer.Context):
    print("Current configuration:")
    print(ctx.obj)

if __name__ == "__main__":
    app()

The callback loads configuration. It stores data in ctx.obj. Commands can access this shared state.

For more on state management, see our Python Typer Global State with Context guide.

Error Handling in Root Callbacks

Proper error handling is crucial. Global callbacks should validate inputs early.

Typer provides several options. You can use typer.BadParameter. Or custom validation logic.

 
import typer
from typing import Optional

app = typer.Typer()

@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    environment: Optional[str] = typer.Option(None, help="Environment to use")
):
    if environment and environment not in ["dev", "prod", "test"]:
        raise typer.BadParameter("Environment must be dev, prod, or test")
    
    ctx.ensure_object(dict)
    ctx.obj["ENV"] = environment or "dev"
    
    if ctx.invoked_subcommand is None:
        print(f"Using {ctx.obj['ENV']} environment")

@app.command()
def deploy(ctx: typer.Context):
    print(f"Deploying to {ctx.obj['ENV']} environment")

if __name__ == "__main__":
    app()

This validates the environment option. It provides helpful error messages.

For comprehensive error strategies, read our Python Typer Error Handling Guide.

Combining with Other Typer Features

Global options work with other Typer features. You can use them with async commands. Or rich output formatting.

Here's an example with rich integration.

 
import typer
from rich.console import Console
from rich.table import Table

app = typer.Typer()
console = Console()

@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    fancy: bool = typer.Option(False, help="Use fancy output")
):
    ctx.ensure_object(dict)
    ctx.obj["fancy"] = fancy
    
    if ctx.invoked_subcommand is None and fancy:
        table = Table(title="Available Commands")
        table.add_column("Command")
        table.add_column("Description")
        table.add_row("list", "Show items")
        table.add_row("add", "Add new item")
        console.print(table)

@app.command()
def list_items(ctx: typer.Context):
    if ctx.obj.get("fancy"):
        console.print("[bold green]Items list:[/bold green]")
        console.print("- Item 1\n- Item 2\n- Item 3")
    else:
        print("Items list:")
        print("- Item 1")
        print("- Item 2")
        print("- Item 3")

if __name__ == "__main__":
    app()

This shows how global options enhance output. The fancy flag changes display format.

Learn more in our Python Typer Rich Integration Guide.

Best Practices

Follow these tips for effective global options.

Keep callbacks focused. They should handle setup and validation. Avoid business logic in callbacks.

Use meaningful default values. Options should have sensible defaults. This improves user experience.

Document your global options. Clear help text is essential. Users need to understand available options.

Test callback behavior thoroughly. Verify both cases. With and without subcommands.

Conclusion

Typer's global options with root callbacks are powerful. They enable shared functionality across commands.

The invoke_without_command parameter adds flexibility. Your CLI can handle both standalone and command modes.

Use this feature for configuration. For validation. And for resource setup. It will make your CLI applications more robust.

Remember to follow best practices. Keep callbacks clean. Test both invocation modes. Your users will appreciate the consistency.