Building CLIs with Typer
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
- argparse-guide — Building CLIs with Python’s standard library
- env-variables — Working with environment variables
- python-type-hints — Adding type annotations to your code