Skip to main content

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.