Python has long been one of the best languages for building command-line tools. Whether you want to automate repetitive tasks, manage files, interact with APIs, or build your own developer utilities, Python provides all the tools you need. But until recently, building a clean and user-friendly CLI required quite a bit of boilerplate.
Typer changes that.
Typer is a modern framework for building powerful, elegant, and highly maintainable command-line applications using Python. Created by the same author behind FastAPI, Typer brings the same philosophy to CLIs:
clean syntax, type hints, automatic documentation, and a great developer experience.
Why Typer?
Unlike older libraries such as argparse, click, or docopt, Typer uses Python’s built-in type hints to automatically:
-
Parse arguments and options
-
Validate input
-
Generate beautiful help messages
-
Support shell autocompletion
-
Reduce boilerplate significantly
In short, Typer lets you build production-grade CLI tools with minimal code.
What You’ll Build in This Tutorial
Throughout this tutorial, you will:
-
Create a basic CLI application
-
Add arguments, options, flags, and prompts
-
Build multiple subcommands
-
Use type hints for validation
-
Add autocompletion support
-
Style your CLI output
-
Package the CLI for distribution
-
Build a real-world example: a Notes Manager CLI
By the end, you’ll have all the knowledge needed to build your own command-line tools for automation, DevOps, daily productivity, or even publish them to PyPI.
Prerequisites
Before diving into Typer and building your command-line applications, make sure you have the following tools and knowledge in place. This tutorial is designed to be beginner-friendly but assumes basic familiarity with Python development.
✔ Python Version
You’ll need Python 3.10 or higher.
Typer works with older versions as well, but using the latest version gives you:
-
Better type hinting
-
Improved performance
-
Access to modern syntax features
Check your version:
python3 --version
✔ Basic Knowledge of Python
You should understand:
-
Functions and parameters
-
Imports and modules
-
Using the terminal or command prompt
No advanced Python skills are required—you’ll learn everything in context.
✔ Terminal or Command Prompt
Since we are building CLI applications, you’ll be running commands frequently.
Any terminal works:
-
macOS Terminal
-
Linux terminal
-
Windows PowerShell
-
Windows Terminal
✔ Optional (But Recommended): Virtual Environment
Using a virtual environment helps isolate project dependencies.
python3 -m venv venv
source venv/bin/activate # macOS/Linux
venv\Scripts\activate # Windows
This ensures your global Python installation stays clean and your project uses the correct package versions.
✔ Install Typer
We’ll install Typer (plus extras like color support) in the next section, but you should have pip available:
pip --version
You're all set!
Setting Up Typer
In this section, you’ll prepare your project structure, install Typer, and run your first simple CLI command. Typer is lightweight and easy to integrate into any Python project.
1. Install Typer
Typer comes in two variants:
-
typer— the core library -
typer[all]— includes tab completion + colorized output dependencies
For full functionality, install the recommended version:
pip install "typer[all]"
You can verify the installation with:
python -c "import typer; print(typer.__version__)"
2. Create the Project Structure
Let’s start with a clean folder. Create a project directory named typer-cli-tutorial:
mkdir typer-cli-tutorial
cd typer-cli-tutorial
Inside the folder, create your first Python file:
typer-cli-tutorial/
│
└── app.py
You can create the file using:
touch app.py
3. Your First Typer Application
Open app.py and add this minimal code:
import typer
app = typer.Typer()
@app.command()
def hello(name: str):
"""Say hello to a user."""
typer.echo(f"Hello, {name} 👋")
if __name__ == "__main__":
app()
How this works:
-
app = typer.Typer()creates the main CLI application. -
@app.command()registers a function as a command. -
Typer automatically turns the function’s parameter (
name: str) into a CLI argument. -
app()runs the CLI.
4. Run Your First Command
From the terminal, run:
python app.py Alice
You should see:
Hello, Alice 👋
Try asking for help:
python app.py --help
You’ll get a fully auto-generated CLI help message, thanks to Typer.
5. What’s Next
You now have a working Typer application!
In the next section, you’ll learn how to build commands with arguments, options, default values, and better documentation.
Building Your First Typer Command
Now that your Typer project is set up, let’s build your first real CLI command. This section will teach you how to create commands, accept arguments, and use automatic help documentation.
1. Understanding Commands in Typer
In Typer, each function becomes a CLI command when decorated with @app.command().
Example:
@app.command()
def greet():
...
This creates a command you can run as:
python app.py greet
2. Adding Arguments
Arguments are required inputs.
Let’s build a simple greet command inside app.py:
import typer
app = typer.Typer()
@app.command()
def greet(name: str):
"""
Greet a user by name.
"""
typer.echo(f"Hello, {name}!")
if __name__ == "__main__":
app()
Run it:
python app.py greet Alice
Output:
Hello, Alice!
✔ Auto-Generated Help
Try:
python app.py greet --help
Typer automatically shows:
Usage: app.py greet [OPTIONS] NAME
That’s the power of type hints + Typer.
3. Adding Optional Arguments (Options)
Options are like flags or parameters that start with --.
Modify your command:
@app.command()
def greet(
name: str,
polite: bool = typer.Option(False, "--polite", help="Use a polite greeting.")
):
"""
Greet a user with an optional polite mode.
"""
if polite:
typer.echo(f"Good day, {name}. It's a pleasure to meet you.")
else:
typer.echo(f"Hey {name}!")
Test it:
Normal:
python app.py greet Alice
Polite mode:
python app.py greet Alice --polite
4. Adding Default Values
@app.command()
def repeat(message: str = "Hello", times: int = 1):
for _ in range(times):
typer.echo(message)
Run:
python app.py repeat
python app.py repeat "Hi there" --times 3
5. Prompting the User for Input
Typer can ask for input interactively:
@app.command()
def login(username: str = typer.Option(..., prompt=True)):
typer.echo(f"Logged in as {username}")
Running:
python app.py login
Will show:
Username:
6. Summary of This Section
You learned how to:
-
Create commands using
@app.command() -
Add required arguments
-
Add optional flags and options
-
Define default values
-
Prompt users interactively
-
Generate automatic help messages
Next, you’ll learn how to build multiple commands and organize them into a clean CLI app.
Adding Multiple Commands
Now that you’ve created basic commands, it’s time to grow your Typer application into a multi-command CLI tool—just like git, docker, or npm, which have subcommands such as:
git add
git commit
git push
Typer makes building multi-command apps natural and clean.
1. Multiple Commands in a Single File
You already did this in Section 4—having both hello and greet commands inside the same app = typer.Typer() instance.
Example:
import typer
app = typer.Typer()
@app.command()
def hello(name: str):
typer.echo(f"Hello, {name} 👋")
@app.command()
def greet(name: str):
typer.echo(f"Greetings, {name}!")
if __name__ == "__main__":
app()
Try:
python app.py --help
You’ll now see:
Commands:
greet
hello
2. Creating Subcommands (Group Commands)
For larger CLI tools, you don’t want everything in one file.
Typer lets you create smaller modules, each with its own commands.
Example project:
typer-cli-tutorial/
│
├── app.py
└── commands/
├── __init__.py
├── users.py
└── notes.py
users.py
import typer
users_app = typer.Typer()
@users_app.command()
def create(username: str):
typer.echo(f"User '{username}' created!")
@users_app.command()
def delete(username: str):
typer.echo(f"User '{username}' deleted!")
notes.py
import typer
notes_app = typer.Typer()
@notes_app.command()
def add(text: str):
typer.echo(f"Note added: {text}")
@notes_app.command()
def list():
typer.echo("Listing all notes...")
app.py (root CLI)
import typer
from commands.users import users_app
from commands.notes import notes_app
app = typer.Typer()
app.add_typer(users_app, name="users")
app.add_typer(notes_app, name="notes")
if __name__ == "__main__":
app()
3. Running Subcommands
Now you can run:
python app.py users create alice
python app.py users delete alice
python app.py notes add "Buy milk"
python app.py notes list
And your help menu will look like:
python app.py --help
Commands:
users
notes
And subcommand help:
python app.py users --help
4. Why Use Multiple Command Groups?
This is the recommended architecture when building:
-
A file management CLI
-
API testing helpers
-
DevOps or automation tools
-
Productivity apps (notes, to-do lists, timers)
-
Data processing pipelines
It makes your CLI:
✔ Modular
✔ Easy to maintain
✔ Easy to test
✔ Easy to scale
5. Real Example Summary
You now know how to:
-
Add multiple commands in one file
-
Organize commands into submodules
-
Group related commands using
add_typer() -
Create scalable CLI architectures
This prepares you for the next topic: Typer’s powerful type hint–based validation.
Type Hints & Validation (Typer’s Superpower)
One of the biggest advantages of Typer—and what makes it stand out from argparse or even Click—is its deep integration with Python type hints.
Typer uses these hints to:
-
Automatically parse CLI input
-
Validate data types
-
Provide better help messages
-
Reduce boilerplate
-
Prevent errors before runtime
Let’s explore how this works.
1. Automatic Type Conversion
Typer converts CLI input into Python types based on your function’s annotations.
Example:
@app.command()
def add(a: int, b: int):
typer.echo(a + b)
Run:
python app.py add 3 7
Output:
10
If someone tries:
python app.py add apple banana
Typer responds with:
Error: Invalid value for 'A': apple is not a valid integer.
You get validation for free.
2. Support for Common Types
Typer supports nearly all built-in Python types:
✔ int
age: int
✔ float
temperature: float
✔ bool
Works automatically for flags:
is_admin: bool = False
3. Using Enum for Predefined Choices
Enums give you dropdown-style restrictions in the CLI.
from enum import Enum
class Role(str, Enum):
admin = "admin"
user = "user"
guest = "guest"
@app.command()
def create_user(name: str, role: Role):
typer.echo(f"{name} created as {role}")
Test it:
python app.py create-user Alice admin
python app.py create-user Bob guest
Help message will show:
[admin|user|guest]
Perfect for category-like arguments.
4. Date & Time with datetime
Typer can parse dates like 2025-01-01 automatically.
from datetime import datetime
@app.command()
def schedule(date: datetime):
typer.echo(f"Scheduled on {date}")
Run:
python app.py schedule 2025-10-12
Invalid dates give automatic validation errors.
5. Optional Types
Optional arguments allow None as a valid value.
from typing import Optional
@app.command()
def info(age: Optional[int] = None):
if age:
typer.echo(f"Age: {age}")
else:
typer.echo("Age not provided")
6. Lists and Multiple Values
Typer supports multiple values:
@app.command()
def tags(tags: list[str]):
typer.echo(tags)
Run:
python app.py tags python typer cli
Output:
['python', 'typer', 'cli']
7. Summary
With type hints, your CLI gets:
✔ Automatic conversion
✔ Automatic error messages
✔ Cleaner code
✔ Stronger documentation
✔ Zero manual validation
This is what makes Typer modern and developer-friendly.
Adding Optional Flags, Default Values, and Prompts
In this section, you’ll learn how to enrich your CLI commands with options, flags, default values, and interactive prompts.
These features make your CLI feel more polished and user-friendly—similar to tools like git, docker, or aws.
1. What Are Options?
Options are optional parameters beginning with --, such as:
--verbose
--count 3
--force
In Typer, options use typer.Option().
2. Boolean Flags (--flag)
Boolean options become flags. If you include the flag, the value is True.
@app.command()
def build(verbose: bool = typer.Option(False, "--verbose", "-v")):
if verbose:
typer.echo("Building in verbose mode...")
else:
typer.echo("Building...")
Try:
python app.py build
python app.py build --verbose
python app.py build -v
3. Options With Default Values
Just set a default in the function parameter:
@app.command()
def repeat(message: str, times: int = 1):
for _ in range(times):
typer.echo(message)
Try:
python app.py repeat "Hello"
python app.py repeat "Hello" --times 3
4. Required Options
Mark an option as required using ...:
@app.command()
def login(username: str = typer.Option(...)):
typer.echo(f"Logged in as {username}")
Now this fails:
python app.py login
Typer:
Error: Missing option '--username'.
5. Adding Help Text
Help text appears automatically in your CLI’s help menu.
@app.command()
def backup(
path: str,
compress: bool = typer.Option(
False,
"--compress",
help="Compress files during backup."
),
):
if compress:
typer.echo(f"Backing up {path} with compression.")
else:
typer.echo(f"Backing up {path}.")
6. Using Prompts (Interactive Input)
Prompts let the CLI ask the user for values interactively.
Useful for passwords, usernames, tokens, etc.
@app.command()
def create_user(
username: str = typer.Option(..., prompt=True),
password: str = typer.Option(..., prompt=True, hide_input=True)
):
typer.echo(f"Created user: {username}")
Run:
python app.py create-user
You’ll see:
Username:
Password:
Both values will be captured securely.
7. Confirmation Prompts
Typer lets you ask the user before performing destructive actions:
@app.command()
def delete(file: str):
if typer.confirm(f"Are you sure you want to delete {file}?"):
typer.echo("File deleted.")
else:
typer.echo("Cancelled.")
8. Default Factories
Sometimes you want dynamic defaults:
import uuid
@app.command()
def generate(id: str = typer.Option(default_factory=lambda: uuid.uuid4().hex)):
typer.echo(f"Generated ID: {id}")
9. Summary
You now know how to use:
✔ Boolean flags (--verbose)
✔ Options with default values
✔ Required options
✔ Interactive prompts
✔ Confirmation prompts
✔ Hidden input
✔ Dynamic default generation
These tools elevate your CLI into a user-friendly and flexible application.
Autocompletion Support
One of the most powerful features of Typer is its built-in autocompletion, which works across major shells:
Bash, Zsh, Fish, and PowerShell.
Autocompletion makes your CLI behave like professional tools such as:
-
git <tab> -
docker <tab> -
aws <tab>
It improves developer experience dramatically—especially for multi-command apps.
1. Autocompletion Works Automatically
Typer uses Click under the hood, which generates shell completion scripts dynamically.
For example, if you have commands:
python app.py g<tab>
It will auto-complete to greet, generate, etc.
But first, you must enable completion in your shell.
2. Install Completion (Recommended)
Typer provides a built-in command for installing completion:
python app.py --install-completion
It will detect your current shell and:
-
Generate a completion script
-
Configure your shell profile
-
Print instructions if manual installation is needed
Example response for Zsh:
Completion installed in ~/.zshrc
Then reload your shell:
source ~/.zshrc
3. Show Completion Script (Optional)
If you want to manually install the script or customize it:
python app.py --show-completion
This prints the generated completion function for your shell.
You can pipe it to a file:
python app.py --show-completion > typer_complete.sh
4. Autocompletion Example
Let’s say your CLI has:
@app.command()
def greet(name: str):
typer.echo(f"Hello, {name}")
Once completion is installed:
python app.py g<tab>
Auto-expands to:
python app.py greet
This works for:
-
Commands
-
Options
-
Enum values
-
Boolean flags
With Enums:
class Role(str, Enum):
admin = "admin"
user = "user"
guest = "guest"
Typing:
python app.py create-user <tab>
Will show:
admin user guest
Zero extra code needed.
5. Completion per Shell
Zsh (macOS/Linux)
python app.py --install-completion
source ~/.zshrc
Bash
python app.py --install-completion
source ~/.bashrc
Fish
python app.py --install-completion
source ~/.config/fish/config.fish
PowerShell
python app.py --install-completion
6. Summary
With Typer, autocompletion is:
✔ Enabled with one command
✔ Works for commands, options, and Enums
✔ Cross-shell compatible
✔ Automatically updated when your CLI changes
This makes your CLI feel professional and easy to use.
Styling Output with Rich (Colors, Tables, Markdown, and More)
A modern CLI tool isn’t just functional — it should be pleasant to use.
That’s where Rich comes in.
Typer integrates beautifully with the Rich library to add:
-
Colored text
-
Styled headers
-
Progress bars
-
Tables
-
Markdown rendering
-
Syntax highlighting
This section shows how to upgrade your CLI’s output from plain to polished.
1. Install Rich
If you installed typer[all], Rich is already included.
If not, install manually:
pip install rich
2. Basic Colored Text
Use Rich’s print instead of typer.echo:
from rich import print
@app.command()
def status():
print("[bold green]Everything is running smoothly![/bold green]")
Run:
python app.py status
3. Using Rich Console (More Control)
from rich.console import Console
console = Console()
@app.command()
def welcome():
console.print("Welcome to the CLI!", style="bold blue")
4. Tables
Tables give structure to CLI output:
from rich.table import Table
from rich.console import Console
console = Console()
@app.command()
def list_users():
table = Table(title="Users")
table.add_column("ID", justify="right")
table.add_column("Name", style="cyan")
table.add_column("Role", style="magenta")
table.add_row("1", "Alice", "Admin")
table.add_row("2", "Bob", "User")
console.print(table)
5. Markdown Support
Rich can render Markdown directly:
from rich.markdown import Markdown
@app.command()
def docs():
md = Markdown("""
# Typer CLI Documentation
This is an example of **Rich-rendered Markdown**.
""")
console.print(md)
6. Progress Bars
Useful for long-running tasks:
from rich.progress import track
import time
@app.command()
def process():
for step in track(range(10), description="Processing..."):
time.sleep(0.3)
7. Syntax Highlighting
For showing code snippets:
from rich.syntax import Syntax
@app.command()
def show_code():
code = """
def hello():
print("Hello world")
"""
syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
console.print(syntax)
8. Logging with Rich
Rich includes a beautiful logging handler:
import logging
from rich.logging import RichHandler
logging.basicConfig(
level="INFO",
handlers=[RichHandler()]
)
@app.command()
def run():
logging.info("Application started")
9. Summary
Rich brings professional UI elements to your CLI:
✔ Colors & text styles
✔ Tables
✔ Progress bars
✔ Markdown
✔ Syntax highlighting
✔ Beautiful logs
Combined with Typer, your CLI will look polished and modern.
Packaging and Distribution
Once your Typer CLI is ready, the next step is making it easy for others to install and use. In this section, you'll learn how to:
-
Package your CLI as an installable Python module
-
Add console entry points
-
Install it globally using
pipx -
Build and publish it to PyPI
-
(Optional) Create a standalone binary with PyInstaller
This is where your CLI becomes a real, distributable tool.
1. Basic Project Structure
A recommended structure for packaging:
typer-cli-tutorial/
│
├── app/
│ ├── __init__.py
│ └── main.py ← your Typer code here
│
├── pyproject.toml
└── README.md
Move your Typer app code into app/main.py:
import typer
app = typer.Typer()
@app.command()
def hello(name: str):
typer.echo(f"Hello, {name} 👋")
def main():
app()
if __name__ == "__main__":
main()
2. Add pyproject.toml (Modern Packaging)
Create pyproject.toml in the project root:
[project]
name = "typer-cli-tutorial"
version = "0.1.0"
description = "A sample Typer CLI app"
readme = "README.md"
authors = [
{ name="Your Name", email="[email protected]" }
]
requires-python = ">=3.10"
dependencies = ["typer[all]"]
[project.scripts]
typercli = "app.main:main"
✔ Key part: Entry point
typercli = "app.main:main"
This lets users run your tool like:
typercli hello Bob
No need for python app.py.
3. Install Locally for Development
Run:
pip install -e .
or better:
pipx install .
Now test it:
typercli hello Alice
4. Building the Package
Install the build tool:
pip install build
Then build:
python -m build
This generates:
-
dist/typer-cli-tutorial-0.1.0.tar.gz -
dist/typer_cli_tutorial-0.1.0-py3-none-any.whl
5. Uploading to PyPI (optional)
Install Twine:
pip install twine
Upload:
twine upload dist/*
Your CLI is now available to everyone via:
pip install typer-cli-tutorial
6. Create a Standalone Binary (Optional)
If you want to distribute a “double-clickable” or portable executable:
pip install pyinstaller
pyinstaller --onefile app/main.py --name typercli
Outputs:
dist/typercli
Users can run it directly:
./typercli hello Bob
7. Summary
You learned how to:
✔ Structure your CLI as a package
✔ Add a CLI entry point
✔ Build a distributable Python wheel
✔ Install locally via pipx
✔ Publish to PyPI
✔ Create standalone binaries
This is everything needed to ship production-grade CLI tools.
Real-World Example — Building a Complete Notes CLI
Now it’s time to combine everything you’ve learned and build a fully functional CLI application:
a Notes Manager that lets users add, list, and delete notes stored in a local JSON file.
This real-world example demonstrates:
-
Multiple commands
-
File handling
-
Structured output (Rich)
-
Type hints
-
Options & prompts
-
A clean project structure
1. Project Structure
Create this structure:
notes-cli/
│
├── app/
│ ├── __init__.py
│ ├── main.py
│ └── notes.py
│
├── notes.json ← created automatically
└── pyproject.toml
2. Notes Storage Helper (notes.py)
import json
from pathlib import Path
NOTES_FILE = Path("notes.json")
def load_notes():
if not NOTES_FILE.exists():
return []
with NOTES_FILE.open("r") as f:
return json.load(f)
def save_notes(notes):
with NOTES_FILE.open("w") as f:
json.dump(notes, f, indent=2)
3. Build the Notes Commands (main.py)
import typer
from rich import print
from rich.table import Table
from .notes import load_notes, save_notes
app = typer.Typer()
@app.command()
def add(text: str):
"""Add a new note."""
notes = load_notes()
notes.append({"text": text})
save_notes(notes)
print(f"[green]Note added:[/green] {text}")
@app.command("list")
def list_notes():
"""List all notes."""
notes = load_notes()
if not notes:
print("[yellow]No notes found.[/yellow]")
return
table = Table(title="Your Notes")
table.add_column("ID", justify="right")
table.add_column("Text", style="cyan")
for idx, note in enumerate(notes, start=1):
table.add_row(str(idx), note["text"])
print(table)
@app.command()
def delete(note_id: int):
"""Delete a note by ID."""
notes = load_notes()
if note_id < 1 or note_id > len(notes):
print("[red]Invalid note ID[/red]")
raise typer.Exit()
note = notes.pop(note_id - 1)
save_notes(notes)
print(f"[red]Deleted:[/red] {note['text']}")
4. Try the CLI
Add a note:
python -m app.main add "Buy groceries"
python -m app.main add "Learn Typer"
List notes:
python -m app.main list
Example output:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
┃ ID ┃ Text ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
│ 1 │ Buy groceries │
│ 2 │ Learn Typer │
└─────────┴──────────────────┘
Delete a note:
python -m app.main delete 1
List again:
python -m app.main list
5. Add Shell Autocompletion
Enable autocomplete for all commands:
python -m app.main --install-completion
6. Add Entry Point (pyproject.toml)
[project]
name = "notes-cli"
version = "0.1.0"
description = "A simple CLI notes app built with Typer"
dependencies = ["typer[all]", "rich"]
[project.scripts]
notes = "app.main:app"
Now install locally:
pip install -e .
Run it globally:
notes add "Read a book"
notes list
notes delete 1
7. Summary
Your Notes CLI now supports:
✔ Adding notes
✔ Listing notes in Rich tables
✔ Deleting notes by ID
✔ Persistent storage via JSON
✔ Autocompletion
✔ Global installation via entry points
You’ve effectively built a full CLI application with Typer — modular, maintainable, and ready to distribute.
Best Practices for Building Typer CLI Applications
To build scalable, maintainable, and professional-grade CLI tools with Typer, you should follow a set of best practices. These guidelines apply whether you're creating a small personal utility or a large multi-command tool distributed to hundreds of developers.
1. Structure Your Project Cleanly
As your CLI grows, avoid putting everything in a single file.
Recommended structure:
project/
│
├── app/
│ ├── __init__.py
│ ├── main.py ← root Typer app
│ ├── commands/
│ │ ├── users.py
│ │ ├── notes.py
│ │ └── tasks.py
│ └── helpers/
│ ├── file.py
│ └── validate.py
│
└── pyproject.toml
Benefits:
✔ Maintainability
✔ Clear separation of concerns
✔ Reusable helper modules
2. Use add_typer() for Grouping Commands
Avoid giant files with dozens of commands.
Instead:
app.add_typer(users_app, name="users")
app.add_typer(notes_app, name="notes")
This mimics professional CLI tools like:
-
docker compose -
git remote -
aws s3
3. Keep Functions Small and Focused
Avoid doing too much inside a single command function.
Good pattern:
def command():
validated = helper_function(input)
result = service_layer(validated)
display(result)
Make your CLI merely orchestrate logic rather than contain all logic.
4. Always Add Help Text
A CLI without good documentation becomes unusable.
Always use:
@app.command(help="Description here")
def something():
...
Or docstrings:
@app.command()
def deploy():
"""
Deploy the application to the server.
"""
Typer automatically shows this in the help menu.
5. Use Type Hints Everywhere
Type hints give you:
-
Automatic validation
-
Built-in documentation
-
Better autocompletion
-
Fewer runtime errors
Example:
def add_user(name: str, role: Role, age: int = 18):
...
6. Validate Inputs When Needed
Even with type hints, some things need custom validation:
-
File path existence
-
Email addresses
-
URL format
-
Value ranges
Use exceptions + typer.Exit():
if age < 0:
typer.echo("Age cannot be negative")
raise typer.Exit(code=1)
7. Use Rich for Formatting
CLI users rely heavily on readable output.
Use:
-
Tables for structured data
-
Colors for important messages
-
Markdown for documentation
-
Syntax highlighting for code snippets
This makes your CLI feel premium.
8. Add Logging for Debugging
Add Rich-powered logging:
import logging
from rich.logging import RichHandler
logging.basicConfig(level="INFO", handlers=[RichHandler()])
Then:
logging.info("Starting task…")
Useful for CLI tools used in CI/CD pipelines.
9. Write Tests Using Typer’s Test Utilities
Typer provides CliRunner() for testing.
Example:
from typer.testing import CliRunner
from app.main import app
runner = CliRunner()
def test_hello():
result = runner.invoke(app, ["hello", "Alice"])
assert "Alice" in result.output
Tests keep your CLI stable as it grows.
10. Consider pipx for Local Installation
Recommend your users install your CLI globally using:
pipx install .
Benefits:
-
No virtual environment needed
-
Global executable
-
Dependencies isolated
11. Make Use of Autocompletion
Always tell users to run:
yourcli --install-completion
This improves usability dramatically.
12. Version Your CLI
Add a version command:
@app.command()
def version():
typer.echo("CLI Version 1.0.0")
Even better—store it in a single source of truth (pyproject.toml).
13. Fail Gracefully
Avoid uncaught exceptions. Use:
try:
...
except Exception as e:
typer.echo(f"Error: {e}")
raise typer.Exit(1)
14. Keep Commands Predictable
Follow common UX patterns:
-
Don’t mix options and arguments unexpectedly
-
Use plural for list commands (
list users) -
Use verbs for actions (
add,delete,update)
Good:
notes add
notes list
notes delete
Not good:
notes create-list
notes rm
notes thingy
Consistency = great UX.
15. Document Everything
Provide:
-
README with usage examples
-
Clear installation instructions
-
Examples for each command
-
Troubleshooting section
Good documentation turns a good CLI into a great one.
Conclusion
You’ve now built a complete understanding of how to create modern, powerful, and professional Python command-line applications using Typer. From simple one-line commands to fully modular, multi-command tools with autocompletion, beautiful Rich output, packaging, testing, and distribution — you now have all the tools needed to ship real-world CLI software.
Here’s what you’ve accomplished in this tutorial:
✔ Built your first Typer commands
You created simple commands, explored arguments, options, flags, and automatic help text generation.
✔ Used Python type hints for validation
Typer’s tight integration with type hints gave you automatic parsing, error handling, and cleaner code.
✔ Created multi-command and modular CLIs
You organized commands into multiple files and used add_typer() to group them into a scalable CLI architecture.
✔ Added interactive user experiences
Prompts, confirmation dialogs, default values, and hidden input improved usability.
✔ Enhanced CLI output with Rich
Color, tables, Markdown, progress bars, syntax highlighting, and structured logs made your CLI look polished and professional.
✔ Enabled shell autocompletion
Your CLI became faster to use and more discoverable with Bash, Zsh, Fish, and PowerShell completion support.
✔ Learned how to package and distribute
You built a pyproject.toml, created entry points, installed your CLI globally with pipx, and even prepared it for PyPI.
✔ Built a real-world Notes CLI
A complete, functional project demonstrating everything—from storage and commands to Rich-powered UI.
✔ Mastered best practices
From modular architecture to testing, logging, validation, and consistent UX conventions.
Why Typer Is a Great Choice
Typer makes CLI development:
-
Fast — less boilerplate
-
Clean — type hints everywhere
-
Powerful — works with modern tooling
-
Developer-friendly — clear errors, help messages, and autocompletion
As your CLI grows, Typer grows with you.
What You Can Build Next
Here are some real-world ideas you can now create confidently:
-
A project generator (like
create-react-app) -
DevOps automation tools
-
API testing utilities
-
File management tools
-
A personal productivity suite (notes, timers, reminders)
-
A deploy script for servers or cloud environments
-
Data processing pipelines
-
Git integrations or code review helpers
Your CLI apps can be shared with your team, published on PyPI, or distributed as binaries.
You can find the full source code on our GitHub.
That's just the basics. If you need more deep learning about Python, Django, FastAPI, Flask, and related, you can take the following cheap course:
- 100 Days of Code: The Complete Python Pro Bootcamp
- Python Mega Course: Build 20 Real-World Apps and AI Agents
- Python for Data Science and Machine Learning Bootcamp
- Python for Absolute Beginners
- Complete Python With DSA Bootcamp + LEETCODE Exercises
- Python Django - The Practical Guide
- Django Masterclass : Build 9 Real World Django Projects
- Full Stack Web Development with Django 5, TailwindCSS, HTMX
- Django - The Complete Course 2025 (Beginner + Advance + AI)
- Ultimate Guide to FastAPI and Backend Development
- Complete FastAPI masterclass from scratch
- Mastering REST APIs with FastAPI
- REST APIs with Flask and Python in 2025
- Python and Flask Bootcamp: Create Websites using Flask!
- The Ultimate Flask Course
Thanks!
