Skip to main content

Implementing Command Callbacks

To execute logic before any subcommand runs—such as initializing shared state or defining global options—use the @app.callback() decorator on a function within your Typer application.

import typer

app = typer.Typer()
state = {"verbose": False}

@app.callback()
def main(verbose: bool = False):
"""
Manage users in the awesome CLI app.
"""
if verbose:
print("Will write verbose output")
state["verbose"] = True

@app.command()
def create(username: str):
if state["verbose"]:
print("About to create a user")
print(f"Creating user: {username}")

if __name__ == "__main__":
app()

How Callbacks Work

The Typer.callback() method allows you to define a function that acts as the entry point for a group of commands.

  • Global Options: Any parameters defined in the callback function (like verbose: bool in the example above) become global options for the entire CLI.
  • Shared State: You can use the callback to modify a global object or a dictionary that subcommands later access to determine their behavior.
  • Help Documentation: The docstring of the function decorated with @app.callback() is automatically used as the main description in the CLI's help output.

Running Callbacks Without Subcommands

By default, a callback only executes if a subcommand is provided. If you want the callback logic to run even when the base command is called alone, set invoke_without_command=True in the decorator.

import typer

app = typer.Typer()

@app.callback(invoke_without_command=True)
def main():
"""
This logic runs even if no subcommand is called.
"""
print("Initializing system components...")

@app.command()
def run():
print("Running task")

if __name__ == "__main__":
app()

Overriding Constructor Callbacks

You can provide a callback directly when initializing the Typer class. However, if you later use the @app.callback() decorator, the decorated function will override the one passed to the constructor.

import typer

def initial_callback():
print("This will be ignored")

# Callback passed in constructor
app = typer.Typer(callback=initial_callback)

@app.callback()
def new_callback():
# This function takes precedence
print("Override callback running")

Troubleshooting: Argument Order

A common issue when using global options defined in a callback is the order of arguments on the command line. Global options must be placed before the subcommand.

  • Correct: python main.py --verbose create hiro
  • Incorrect: python main.py create hiro --verbose

If the option is placed after the subcommand, Typer will attempt to find that option on the create command instead of the global callback, resulting in an "No such option" error if the subcommand does not explicitly define it.