Building CLIs with Typer

· 4 min read · Updated March 14, 2026 · beginner
python typer cli libraries

Typer is a library for building CLI applications that feels like using Python itself. It uses type annotations to define commands and arguments, so you get validation, autocomplete, and clean code without boilerplate.

Why Typer?

If you’ve used Click, Typer will feel familiar—it actually builds on Click under the hood. The key difference is how you define arguments:

import typer

app = typer.Typer()

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

if __name__ == "__main__":
    app()
$ python hello.py Alice
Hello, Alice!

No decorators for arguments, no click.Argument() or click.Option(). Just function signatures with type hints.

Installation

Install Typer with pip:

pip install typer

For full functionality including shell completion support, install the optional dependencies:

pip install "typer[all]"

Your First Typer App

Create a simple CLI that takes a name and optional greeting:

#!/usr/bin/env python3
import typer

app = typer.Typer()

@app.command()
def main(name: str = "World", greeting: str = "Hello"):
    """Say hello to someone."""
    print(f"{greeting}, {name}!")

if __name__ == "__main__":
    app()
$ python hello.py --help
 Usage: hello.py [OPTIONS] NAME [GREETING]
 
 Say hello to someone.
 
 Arguments:
   NAME       The name to greet
   GREETING   The greeting to use (default: Hello)
 
 Options:
   --help     Show this command and exit

The help text is auto-generated from your function signature and docstring.

Positional and Optional Arguments

Typer infers argument behavior from defaults and types:

import typer

app = typer.Typer()

@app.command()
def greet(
    name: str,                    # Required positional
    times: int = 1,               # Optional with default
    verbose: bool = False,        # Boolean flag (--verbose/--no-verbose)
):
    for _ in range(times):
        print(f"Hello, {name}!")

if __name__ == "__main__":
    app()
$ python greet.py Alice
Hello, Alice!

$ python greet.py Alice --times 3
Hello, Alice!
Hello, Alice!
Hello, Alice!

$ python greet.py Bob --verbose
Hello, Bob!

Using Option

For more explicit control, use typer.Option:

import typer

app = typer.Typer()

@app.command()
def create(
    name: str = typer.Option(..., prompt=True),
    email: str = typer.Option(..., prompt=True),
    password: str = typer.Option(..., prompt=True, hide_input=True),
):
    """Create a new user account."""
    print(f"Creating user: {name} ({email})")

if __name__ == "__main__":
    app()

This prompts for missing values interactively.

Type Conversion

Typer automatically converts types, just like argparse:

import typer

app = typer.Typer()

@app.command()
def process(count: int, rate: float, enabled: bool):
    print(f"Count: {count} (type: {type(count).__name__})")
    print(f"Rate: {rate} (type: {type(rate).__name__})")
    print(f"Enabled: {enabled} (type: {type(enabled).__name__})")

if __name__ == "__main__":
    app()
$ python process.py 10 2.5 true
Count: 10 (type: int)
Rate: 2.5 (type: float)
Enabled: True (type: bool)

Invalid types are rejected automatically.

Choices

Restrict values using Python’s Enum or literal types:

from enum import Enum
import typer

class Level(str, Enum):
    EASY = "easy"
    MEDIUM = "medium"
    HARD = "hard"

app = typer.Typer()

@app.command()
def play(level: Level):
    print(f"Starting game at {level.value} difficulty")

if __name__ == "__main__":
    app()
$ python game.py easy
Starting game at easy difficulty

$ python game.py impossible
Usage: game.py [OPTIONS]
Try 'game.py --help' for more info.
Error: Invalid value for 'LEVEL': 'impossible' is not one of 'easy', 'medium', 'hard'.

Subcommands

Typer supports subcommands by grouping multiple commands:

import typer

app = typer.Typer()

@app.command()
def add(x: int, y: int):
    """Add two numbers."""
    print(f"Result: {x + y}")

@app.command()
def multiply(x: int, y: int):
    """Multiply two numbers."""
    print(f"Result: {x * y}")

if __name__ == "__main__":
    app()
$ python calc.py add 5 3
Result: 8

$ python calc.py multiply 5 3
Result: 15

For more complex CLIs with multiple groups of commands, create separate apps and combine them:

users_app = typer.Typer()
items_app = typer.Typer()

app = typer.Typer()
app.add_typer(users_app, name="users")
app.add_typer(items_app, name="items")

Rich Output

Typer integrates with Rich for prettier output:

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

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

@app.command()
def list_users():
    table = Table("ID", "Name", "Email")
    table.add_row("1", "Alice", "alice@example.com")
    table.add_row("2", "Bob", "bob@example.com")
    console.print(table)

if __name__ == "__main__":
    app()

File Arguments

Use typer.FileText or typer.FileBinary for file arguments:

import typer
from typing import Optional

app = typer.Typer()

@app.command()
def count_words(input_file: typer.FileText = "-"):
    """Count words in a file (use - for stdin)."""
    content = input_file.read()
    word_count = len(content.split())
    print(f"Words: {word_count}")

if __name__ == "__main__":
    app()

Type Annotations for Help

Typer uses type annotations but you can override help text:

import typer

app = typer.Typer()

@app.command()
def serve(
    host: str = "127.0.0.1": "IP address to bind to",
    port: int = 8000: "Port number to listen on",
):
    print(f"Starting server on {host}:{port}")

if __name__ == "__main__":
    app()

Exit Codes

Typer exits with appropriate codes on success or error:

import typer

app = typer.Typer()

@app.command()
def fail():
    raise typer.Exit(code=1)

@app.command()
def success():
    print("Done")

See Also