python-cli-typer
SKILL.md
Python CLI with Typer
Build modern, type-safe command-line applications using Typer with automatic help generation, shell completion, and rich terminal output.
When to Use This Skill
- Creating new CLI applications in Python
- Adding command-line interfaces to existing projects
- Migrating from argparse or click to Typer
- Building CLI tools with subcommands
- Adding rich terminal output and progress bars
Quick Start
Installation
uv add typer
# Optional: rich output support
uv add "typer[all]"
Minimal Example
#!/usr/bin/env python3
"""Simple CLI application."""
import typer
app = typer.Typer()
@app.command()
def hello(name: str) -> None:
"""Say hello to NAME."""
typer.echo(f"Hello, {name}!")
if __name__ == "__main__":
app()
Running the CLI
# Run directly
uv run python cli.py hello World
# Or install as package and run
uv run my-cli hello World
CLI Structure Patterns
Single Command CLI
"""Single command application."""
import typer
def main(
name: str = typer.Argument(..., help="Name to greet"),
count: int = typer.Option(1, "--count", "-c", help="Number of greetings"),
uppercase: bool = typer.Option(False, "--uppercase", "-u", help="Uppercase output"),
) -> None:
"""Greet someone multiple times."""
greeting = f"Hello, {name}!"
if uppercase:
greeting = greeting.upper()
for _ in range(count):
typer.echo(greeting)
if __name__ == "__main__":
typer.run(main)
Multi-Command CLI
"""Multi-command application with subcommands."""
import typer
app = typer.Typer(help="Module management CLI")
@app.command()
def list() -> None:
"""List all modules."""
typer.echo("Listing modules...")
@app.command()
def add(name: str, version: str = "latest") -> None:
"""Add a new module."""
typer.echo(f"Adding {name}@{version}")
@app.command()
def remove(name: str, force: bool = typer.Option(False, "--force", "-f")) -> None:
"""Remove a module."""
if force:
typer.echo(f"Force removing {name}")
else:
typer.echo(f"Removing {name}")
if __name__ == "__main__":
app()
Nested Subcommands
"""CLI with nested command groups."""
import typer
# Main app
app = typer.Typer(help="Infrastructure orchestrator CLI")
# Subcommand groups
module_app = typer.Typer(help="Module management commands")
workflow_app = typer.Typer(help="Workflow management commands")
config_app = typer.Typer(help="Configuration commands")
# Register subcommand groups
app.add_typer(module_app, name="module")
app.add_typer(workflow_app, name="workflow")
app.add_typer(config_app, name="config")
# Module commands
@module_app.command("list")
def module_list() -> None:
"""List all modules."""
typer.echo("Modules: ...")
@module_app.command("add")
def module_add(name: str) -> None:
"""Add a module."""
typer.echo(f"Adding module: {name}")
# Workflow commands
@workflow_app.command("run")
def workflow_run(name: str) -> None:
"""Run a workflow."""
typer.echo(f"Running workflow: {name}")
@workflow_app.command("status")
def workflow_status() -> None:
"""Check workflow status."""
typer.echo("Workflow status: OK")
# Config commands
@config_app.command("show")
def config_show() -> None:
"""Show current configuration."""
typer.echo("Config: ...")
@config_app.command("set")
def config_set(key: str, value: str) -> None:
"""Set a configuration value."""
typer.echo(f"Setting {key}={value}")
if __name__ == "__main__":
app()
Usage:
orchestrator module list
orchestrator module add my-module
orchestrator workflow run deploy
orchestrator config set debug true
Arguments and Options
Arguments (Positional)
import typer
from typing import Optional
app = typer.Typer()
@app.command()
def process(
# Required argument
filename: str = typer.Argument(..., help="File to process"),
# Optional argument with default
output: str = typer.Argument("output.txt", help="Output file"),
# Optional argument that can be None
config: Optional[str] = typer.Argument(None, help="Optional config file"),
) -> None:
"""Process a file."""
typer.echo(f"Processing {filename} -> {output}")
if config:
typer.echo(f"Using config: {config}")
Options (Flags)
import typer
from typing import Optional
from pathlib import Path
app = typer.Typer()
@app.command()
def deploy(
# Required option
environment: str = typer.Option(..., "--env", "-e", help="Target environment"),
# Optional with default
timeout: int = typer.Option(300, "--timeout", "-t", help="Timeout in seconds"),
# Boolean flag
dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Simulate without changes"),
# Short and long form
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
# Path option with validation
config: Optional[Path] = typer.Option(
None,
"--config",
"-c",
help="Config file path",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
),
# Multiple values
tags: Optional[list[str]] = typer.Option(None, "--tag", "-t", help="Tags to apply"),
) -> None:
"""Deploy to an environment."""
typer.echo(f"Deploying to {environment}")
typer.echo(f"Timeout: {timeout}s, Dry run: {dry_run}")
if config:
typer.echo(f"Config: {config}")
if tags:
typer.echo(f"Tags: {', '.join(tags)}")
Enums for Choices
import typer
from enum import Enum
class Environment(str, Enum):
development = "development"
staging = "staging"
production = "production"
class LogLevel(str, Enum):
debug = "DEBUG"
info = "INFO"
warning = "WARNING"
error = "ERROR"
app = typer.Typer()
@app.command()
def deploy(
env: Environment = typer.Option(
Environment.development,
"--env",
"-e",
help="Target environment",
case_sensitive=False,
),
log_level: LogLevel = typer.Option(
LogLevel.info,
"--log-level",
"-l",
help="Logging level",
),
) -> None:
"""Deploy with environment choice."""
typer.echo(f"Deploying to {env.value} with log level {log_level.value}")
Input and Output
User Prompts
import typer
app = typer.Typer()
@app.command()
def setup() -> None:
"""Interactive setup wizard."""
# Simple prompt
name = typer.prompt("What's your project name?")
# Prompt with default
author = typer.prompt("Author name", default="Anonymous")
# Hidden input for secrets
api_key = typer.prompt("API Key", hide_input=True)
# Confirmation prompt
confirmed = typer.confirm("Create project?", default=True)
if confirmed:
typer.echo(f"Creating project: {name} by {author}")
else:
typer.echo("Cancelled")
raise typer.Abort()
Styled Output
import typer
app = typer.Typer()
@app.command()
def status() -> None:
"""Show status with colors."""
# Colored text
typer.echo(typer.style("Success!", fg=typer.colors.GREEN, bold=True))
typer.echo(typer.style("Warning!", fg=typer.colors.YELLOW))
typer.echo(typer.style("Error!", fg=typer.colors.RED, bold=True))
# Shortcuts
typer.secho("Green text", fg=typer.colors.GREEN)
typer.secho("Bold red", fg=typer.colors.RED, bold=True)
typer.secho("Blue background", bg=typer.colors.BLUE)
@app.command()
def report() -> None:
"""Show a formatted report."""
typer.secho("=" * 40, fg=typer.colors.CYAN)
typer.secho(" DEPLOYMENT REPORT", fg=typer.colors.CYAN, bold=True)
typer.secho("=" * 40, fg=typer.colors.CYAN)
typer.echo()
typer.secho("✓ Module A deployed", fg=typer.colors.GREEN)
typer.secho("✓ Module B deployed", fg=typer.colors.GREEN)
typer.secho("✗ Module C failed", fg=typer.colors.RED)
Rich Output (with rich library)
import typer
from rich.console import Console
from rich.table import Table
from rich.progress import track
from rich.panel import Panel
import time
app = typer.Typer()
console = Console()
@app.command()
def modules() -> None:
"""List modules in a table."""
table = Table(title="Modules")
table.add_column("Name", style="cyan")
table.add_column("Version", style="green")
table.add_column("Status", style="magenta")
table.add_row("module-a", "1.0.0", "✓ Deployed")
table.add_row("module-b", "2.1.0", "✓ Deployed")
table.add_row("module-c", "0.5.0", "⏳ Pending")
console.print(table)
@app.command()
def process(count: int = 100) -> None:
"""Process with progress bar."""
for _ in track(range(count), description="Processing..."):
time.sleep(0.01)
console.print(Panel.fit("✓ Processing complete!", style="green"))
Error Handling
Exit Codes
import typer
app = typer.Typer()
@app.command()
def validate(config: str) -> None:
"""Validate a configuration file."""
if not config.endswith(".yaml"):
typer.secho("Error: Config must be a YAML file", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1)
try:
# Validate logic...
typer.secho("✓ Configuration is valid", fg=typer.colors.GREEN)
except Exception as e:
typer.secho(f"Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=2)
Abort and Exceptions
import typer
app = typer.Typer()
@app.command()
def delete(name: str, force: bool = False) -> None:
"""Delete a resource."""
if not force:
typer.confirm(f"Delete {name}?", abort=True)
typer.echo(f"Deleting {name}...")
@app.command()
def dangerous() -> None:
"""A dangerous operation."""
typer.secho("This will destroy everything!", fg=typer.colors.RED)
raise typer.Abort()
Callbacks and Context
App Callback (Global Options)
import typer
from typing import Optional
app = typer.Typer()
# Global state
state = {"verbose": False, "config": None}
@app.callback()
def main(
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
config: Optional[str] = typer.Option(None, "--config", "-c", help="Config file"),
) -> None:
"""
Orchestrator CLI - Manage infrastructure modules.
Use --verbose for detailed output.
"""
state["verbose"] = verbose
state["config"] = config
if verbose:
typer.echo("Verbose mode enabled")
@app.command()
def deploy(module: str) -> None:
"""Deploy a module."""
if state["verbose"]:
typer.echo(f"Config: {state['config']}")
typer.echo(f"Deploying module: {module}")
else:
typer.echo(f"Deploying {module}...")
Context for State
import typer
from typing import Optional
app = typer.Typer()
@app.callback()
def main(ctx: typer.Context, debug: bool = False) -> None:
"""Main entry point."""
ctx.ensure_object(dict)
ctx.obj["debug"] = debug
@app.command()
def run(ctx: typer.Context, name: str) -> None:
"""Run a task."""
if ctx.obj["debug"]:
typer.echo(f"DEBUG: Running {name}")
typer.echo(f"Running {name}")
Project Structure
Recommended Layout
src/orchestrator/
├── __init__.py
├── __main__.py # Entry point: python -m orchestrator
├── cli/
│ ├── __init__.py
│ ├── main.py # Main app and callback
│ ├── module.py # Module subcommands
│ ├── workflow.py # Workflow subcommands
│ └── config.py # Config subcommands
└── core/
├── __init__.py
└── ... # Business logic
__main__.py
"""Entry point for python -m orchestrator."""
from orchestrator.cli.main import app
if __name__ == "__main__":
app()
cli/main.py
"""Main CLI application."""
import typer
from orchestrator.cli import module, workflow, config
app = typer.Typer(
name="orchestrator",
help="Infrastructure orchestrator CLI",
no_args_is_help=True,
)
# Register subcommand groups
app.add_typer(module.app, name="module")
app.add_typer(workflow.app, name="workflow")
app.add_typer(config.app, name="config")
@app.callback()
def main(
ctx: typer.Context,
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Infrastructure Orchestrator - Manage modules and workflows."""
ctx.ensure_object(dict)
ctx.obj["verbose"] = verbose
@app.command()
def version() -> None:
"""Show version information."""
typer.echo("orchestrator v1.0.0")
cli/module.py
"""Module management commands."""
import typer
app = typer.Typer(help="Module management commands")
@app.command("list")
def list_modules() -> None:
"""List all registered modules."""
typer.echo("Listing modules...")
@app.command()
def add(name: str, version: str = "latest") -> None:
"""Add a new module."""
typer.echo(f"Adding module: {name}@{version}")
@app.command()
def remove(name: str) -> None:
"""Remove a module."""
typer.echo(f"Removing module: {name}")
pyproject.toml Entry Point
[project.scripts]
orchestrator = "orchestrator.cli.main:app"
Testing CLI
"""Test CLI commands."""
from typer.testing import CliRunner
from orchestrator.cli.main import app
runner = CliRunner()
def test_version() -> None:
"""Test version command."""
result = runner.invoke(app, ["version"])
assert result.exit_code == 0
assert "1.0.0" in result.stdout
def test_module_list() -> None:
"""Test module list command."""
result = runner.invoke(app, ["module", "list"])
assert result.exit_code == 0
def test_module_add() -> None:
"""Test module add command."""
result = runner.invoke(app, ["module", "add", "my-module", "--version", "1.0.0"])
assert result.exit_code == 0
assert "my-module" in result.stdout
def test_missing_required_arg() -> None:
"""Test error on missing required argument."""
result = runner.invoke(app, ["module", "add"])
assert result.exit_code != 0
def test_verbose_flag() -> None:
"""Test verbose flag propagation."""
result = runner.invoke(app, ["--verbose", "module", "list"])
assert result.exit_code == 0
Resources
- Typer documentation
- Click documentation (Typer is built on Click)
- Rich library for beautiful terminal output
Guidelines
- Use type hints for all arguments and options
- Provide help text for every command, argument, and option
- Use
no_args_is_help=Trueon the main app - Group related commands with
add_typer() - Use enums for restricted choices
- Return proper exit codes (0=success, 1+=error)
- Test CLI with
typer.testing.CliRunner - Keep business logic separate from CLI code
Weekly Installs
1
Repository
franciscosanche…factu-esFirst Seen
12 days ago
Security Audits
Installed on
mcpjam1
claude-code1
junie1
windsurf1
zencoder1
crush1