Core Application
The core of any Typer application revolves around the Typer class and the run() function. These tools allow you to transition from simple single-command scripts to complex, nested CLI suites with shared state and global options.
In this tutorial, you will build a user management CLI that evolves from a basic script into a modular application.
Prerequisites
To follow this tutorial, you need typer installed in your environment:
pip install typer
Step 1: Create a Simple Script with typer.run()
For small scripts where you only need a single command, you can use the typer.run() convenience function. This automatically creates a Typer application, registers your function as the main command, and executes it.
Create a file named main.py:
import typer
def main(name: str):
print(f"Hello {name}")
if __name__ == "__main__":
typer.run(main)
When you run this script, Typer uses the function's signature to generate the CLI interface:
$ python main.py --help
Usage: main.py [OPTIONS] NAME
Arguments:
NAME [required]
$ python main.py Camila
Hello Camila
Step 2: Build a Multi-Command App with typer.Typer
As your application grows, you will want multiple subcommands. For this, use the typer.Typer class and the @app.command() decorator.
Create a file named users.py:
import typer
app = typer.Typer()
@app.command()
def create(user_name: str):
print(f"Creating user: {user_name}")
@app.command()
def delete(user_name: str):
print(f"Deleting user: {user_name}")
if __name__ == "__main__":
app()
By calling app() inside the if __name__ == "__main__": block, you trigger the command-line parsing logic. You now have two subcommands:
$ python users.py create "John Doe"
Creating user: John Doe
$ python users.py delete "John Doe"
Deleting user: John Doe
Step 3: Add Global Options with @app.callback()
Often, you need options that apply to the entire application rather than a specific subcommand (e.g., a --verbose flag). You can define these using the @app.callback() decorator.
Update users.py to include a callback for shared state:
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()
The callback function runs before any subcommand. Note that the docstring of the callback function becomes the main help text for the CLI.
$ python users.py --verbose create "John Doe"
Will write verbose output
About to create a user
Creating user: John Doe
Step 4: Modularize with app.add_typer()
For large projects, you can split your CLI into multiple files and merge them using app.add_typer(). This allows you to nest one Typer instance inside another as a subcommand group.
Suppose you have another file items.py:
import typer
app = typer.Typer()
@app.command()
def add(item: str):
print(f"Adding item: {item}")
You can combine users.py and items.py into a main entry point:
import typer
import items
import users
app = typer.Typer()
# Nest the apps under "users" and "items" subcommands
app.add_typer(users.app, name="users")
app.add_typer(items.app, name="items")
if __name__ == "__main__":
app()
Now your CLI has a hierarchy:
$ python main.py users create "John Doe"
Creating user: John Doe
$ python main.py items add "Laptop"
Adding item: Laptop
Important Note on add_typer
When using add_typer(), if you do not provide a name, the sub-app's commands are added directly to the parent app. However, if the sub-app has its own @app.callback(), that callback will be ignored unless the sub-app is added with a specific name.
Summary
You have built a modular CLI application using the core components of Typer:
typer.run(): For quick, single-function scripts.typer.Typer(): The main class for managing commands and sub-apps.@app.command(): Registers functions as subcommands.@app.callback(): Handles global options and shared logic.app.add_typer(): Enables complex, nested command structures.
Next, you can explore how to use Annotated types to add detailed metadata and validation to your command arguments.