
Python Cli Patterns
- 2 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
python-cli-patterns is a Claude Code skill for building Python command-line applications with Typer and Rich.
About
python-cli-patterns is a Claude Code skill for building Python command-line applications with Typer and Rich. It covers basic Typer apps, command groups, arguments and options, Rich table and progress output, and CLI error handling, with a bundled full CLI template and reference files. A developer uses it when writing a terminal application in Python.
- Modern Python CLI development with Typer and Rich
- Covers commands, command groups, Rich tables/progress, and error handling
- Bundles a full CLI template plus Typer, Rich, and configuration references
Python Cli Patterns by the numbers
- 2 all-time installs (skills.sh)
- Ranked #445 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
python-cli-patterns capabilities & compatibility
Free; a pattern/template skill with no external service
- Capabilities
- cli development · terminal ui
- Use cases
- api development
- Pricing
- Free
What python-cli-patterns says it does
Modern CLI development with Typer and Rich.
CLI application patterns for Python. Triggers on: cli, command line, typer, click, argparse, terminal, rich, console, terminal ui.
npx skills add https://github.com/aiskillstore/marketplace --skill python-cli-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Build Python command-line apps with Typer and Rich, including commands, options, and formatted output.
Who is it for?
Writing modern Python CLIs with Typer commands and Rich formatted output
Skip if: Non-Python CLIs or GUI applications
When should I use this skill?
Building a Python CLI, or using typer, click, argparse, rich, or a terminal UI
What you get
A structured Python CLI with commands, options, formatted tables, and error handling.
- Typer-based CLI application
- Rich-formatted console output
By the numbers
- requires Python 3.10+
- bundles a full cli-template.py plus 3 reference files
Files
Python CLI Patterns
Modern CLI development with Typer and Rich.
Basic Typer App
import typer
app = typer.Typer(
name="myapp",
help="My awesome CLI application",
add_completion=True,
)
@app.command()
def hello(
name: str = typer.Argument(..., help="Name to greet"),
count: int = typer.Option(1, "--count", "-c", help="Times to greet"),
loud: bool = typer.Option(False, "--loud", "-l", help="Uppercase"),
):
"""Say hello to someone."""
message = f"Hello, {name}!"
if loud:
message = message.upper()
for _ in range(count):
typer.echo(message)
if __name__ == "__main__":
app()Command Groups
import typer
app = typer.Typer()
users_app = typer.Typer(help="User management commands")
app.add_typer(users_app, name="users")
@users_app.command("list")
def list_users():
"""List all users."""
typer.echo("Listing users...")
@users_app.command("create")
def create_user(name: str, email: str):
"""Create a new user."""
typer.echo(f"Creating user: {name} <{email}>")
@app.command()
def version():
"""Show version."""
typer.echo("1.0.0")
# Usage: myapp users list
# myapp users create "John" "john@example.com"
# myapp versionRich Output
from rich.console import Console
from rich.table import Table
from rich.progress import track
from rich.panel import Panel
import typer
console = Console()
@app.command()
def show_users():
"""Display users in a table."""
table = Table(title="Users")
table.add_column("ID", style="cyan")
table.add_column("Name", style="green")
table.add_column("Email")
users = [
(1, "Alice", "alice@example.com"),
(2, "Bob", "bob@example.com"),
]
for id, name, email in users:
table.add_row(str(id), name, email)
console.print(table)
@app.command()
def process():
"""Process items with progress bar."""
items = list(range(100))
for item in track(items, description="Processing..."):
do_something(item)
console.print("[green]Done![/green]")Error Handling
import typer
from rich.console import Console
console = Console()
def error(message: str, code: int = 1):
"""Print error and exit."""
console.print(f"[red]Error:[/red] {message}")
raise typer.Exit(code)
@app.command()
def process(file: str):
"""Process a file."""
if not os.path.exists(file):
error(f"File not found: {file}")
try:
result = process_file(file)
console.print(f"[green]Success:[/green] {result}")
except ValueError as e:
error(str(e))Quick Reference
| Feature | Typer Syntax |
|---|---|
| Required arg | name: str |
| Optional arg | name: str = "default" |
| Option | typer.Option(default, "--flag", "-f") |
| Argument | typer.Argument(..., help="...") |
| Boolean flag | verbose: bool = False |
| Enum choice | color: Color = Color.red |
| Rich Feature | Usage |
|---|---|
| Table | Table() + add_column/row |
| Progress | track(items) |
| Colors | [red]text[/red] |
| Panel | Panel("content", title="Title") |
Additional Resources
./references/typer-patterns.md- Advanced Typer patterns./references/rich-output.md- Rich tables, progress, formatting./references/configuration.md- Config files, environment variables
Assets
./assets/cli-template.py- Full CLI application template
---
See Also
Related Skills:
python-typing-patterns- Type hints for CLI argumentspython-observability-patterns- Logging for CLI applications
Complementary Skills:
python-env- Package CLI for distribution
"""
CLI Application Template
A production-ready CLI application structure.
Usage:
python cli.py --help
python cli.py greet "World"
python cli.py config init
"""
import sys
from pathlib import Path
from typing import Annotated, Optional
import typer
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import track
# =============================================================================
# App Setup
# =============================================================================
app = typer.Typer(
name="myapp",
help="My awesome CLI application",
no_args_is_help=True,
add_completion=True,
rich_markup_mode="rich",
)
console = Console()
err_console = Console(stderr=True)
# Sub-applications
config_app = typer.Typer(help="Configuration commands")
app.add_typer(config_app, name="config")
# =============================================================================
# State and Configuration
# =============================================================================
class AppState:
"""Application state shared across commands."""
def __init__(self):
self.verbose: bool = False
self.config_dir: Path = Path.home() / ".config" / "myapp"
self.config_file: Path = self.config_dir / "config.toml"
state = AppState()
@app.callback()
def main(
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
config: Optional[Path] = typer.Option(
None, "--config", "-c", help="Config file path"
),
):
"""
[bold blue]MyApp[/bold blue] - A sample CLI application.
Use [green]--help[/green] on any command for more info.
"""
state.verbose = verbose
if config:
state.config_file = config
# =============================================================================
# Utility Functions
# =============================================================================
def log(message: str, style: str = ""):
"""Log message if verbose mode is enabled."""
if state.verbose:
console.print(f"[dim]{message}[/dim]", style=style)
def error(message: str, code: int = 1) -> None:
"""Print error and exit."""
err_console.print(f"[red]Error:[/red] {message}")
raise typer.Exit(code)
def success(message: str) -> None:
"""Print success message."""
console.print(f"[green]✓[/green] {message}")
# =============================================================================
# Commands
# =============================================================================
@app.command()
def greet(
name: Annotated[str, typer.Argument(help="Name to greet")],
count: Annotated[int, typer.Option("--count", "-n", help="Times to greet")] = 1,
loud: Annotated[bool, typer.Option("--loud", "-l", help="Uppercase")] = False,
):
"""
Say hello to someone.
Example:
myapp greet World
myapp greet World --count 3 --loud
"""
message = f"Hello, {name}!"
if loud:
message = message.upper()
for _ in range(count):
console.print(message)
@app.command()
def process(
files: Annotated[
list[Path],
typer.Argument(
help="Files to process",
exists=True,
readable=True,
),
],
output: Annotated[
Optional[Path],
typer.Option("--output", "-o", help="Output file"),
] = None,
):
"""
Process one or more files.
Example:
myapp process file1.txt file2.txt -o output.txt
"""
log(f"Processing {len(files)} files")
results = []
for file in track(files, description="Processing..."):
log(f"Processing: {file}")
# Simulate processing
results.append(f"Processed: {file.name}")
if output:
output.write_text("\n".join(results))
success(f"Results written to {output}")
else:
for result in results:
console.print(result)
@app.command()
def status():
"""Show application status."""
table = Table(title="Application Status")
table.add_column("Setting", style="cyan")
table.add_column("Value", style="green")
table.add_row("Config Dir", str(state.config_dir))
table.add_row("Config File", str(state.config_file))
table.add_row("Verbose", str(state.verbose))
table.add_row(
"Config Exists",
"✓" if state.config_file.exists() else "✗"
)
console.print(table)
# =============================================================================
# Config Subcommands
# =============================================================================
@config_app.command("init")
def config_init(
force: Annotated[
bool,
typer.Option("--force", "-f", help="Overwrite existing"),
] = False,
):
"""Initialize configuration file."""
if state.config_file.exists() and not force:
if not typer.confirm(f"Config exists at {state.config_file}. Overwrite?"):
raise typer.Abort()
state.config_dir.mkdir(parents=True, exist_ok=True)
default_config = """
# MyApp Configuration
# See documentation for all options
[general]
verbose = false
[server]
host = "localhost"
port = 8080
""".strip()
state.config_file.write_text(default_config)
success(f"Created config: {state.config_file}")
@config_app.command("show")
def config_show():
"""Show current configuration."""
if not state.config_file.exists():
error(f"Config not found: {state.config_file}")
content = state.config_file.read_text()
console.print(Panel(content, title=str(state.config_file), border_style="blue"))
@config_app.command("path")
def config_path():
"""Print config file path."""
typer.echo(state.config_file)
# =============================================================================
# Version
# =============================================================================
def version_callback(value: bool):
if value:
console.print("myapp version [bold]1.0.0[/bold]")
raise typer.Exit()
@app.callback()
def version_option(
version: Annotated[
bool,
typer.Option(
"--version",
callback=version_callback,
is_eager=True,
help="Show version",
),
] = False,
):
pass
# =============================================================================
# Entry Point
# =============================================================================
if __name__ == "__main__":
app()
CLI Configuration Patterns
Configuration file and environment variable handling.
Environment Variables
import os
import typer
app = typer.Typer()
@app.command()
def connect(
# Read from env var with fallback
host: str = typer.Option(
"localhost",
envvar="DB_HOST",
help="Database host",
),
port: int = typer.Option(
5432,
envvar="DB_PORT",
help="Database port",
),
# Multiple envvars (first found wins)
password: str = typer.Option(
..., # Required
envvar=["DB_PASSWORD", "DATABASE_PASSWORD", "PGPASSWORD"],
help="Database password",
),
):
"""Connect to database."""
typer.echo(f"Connecting to {host}:{port}")Configuration File with TOML
import tomllib # Python 3.11+
from pathlib import Path
from dataclasses import dataclass
from typing import Optional
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
log_level: str = "INFO"
@classmethod
def load(cls, path: Path | None = None) -> "Config":
"""Load config from TOML file."""
if path is None:
# Search default locations
for p in [
Path("config.toml"),
Path.home() / ".config" / "myapp" / "config.toml",
]:
if p.exists():
path = p
break
if path and path.exists():
with open(path, "rb") as f:
data = tomllib.load(f)
return cls(**data)
return cls()
# Usage in CLI
@app.callback()
def main(
ctx: typer.Context,
config: Path = typer.Option(
None,
"--config", "-c",
exists=True,
help="Config file path",
),
):
ctx.obj = Config.load(config)
@app.command()
def serve(ctx: typer.Context):
config = ctx.obj
typer.echo(f"Starting on {config.host}:{config.port}")Config with Pydantic Settings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
from pathlib import Path
class Settings(BaseSettings):
"""Application settings from env vars and config file."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="MYAPP_", # MYAPP_HOST, MYAPP_PORT
case_sensitive=False,
)
host: str = "localhost"
port: int = 8080
debug: bool = False
database_url: str = Field(
default="sqlite:///app.db",
validation_alias="DATABASE_URL", # Also check DATABASE_URL without prefix
)
api_key: str = Field(default="")
# Load once
settings = Settings()
@app.command()
def serve():
typer.echo(f"Host: {settings.host}")
typer.echo(f"Debug: {settings.debug}")XDG Config Directories
from pathlib import Path
import os
def get_config_dir(app_name: str) -> Path:
"""Get XDG-compliant config directory."""
if os.name == "nt": # Windows
base = Path(os.environ.get("APPDATA", Path.home()))
else: # Linux/macOS
base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
config_dir = base / app_name
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir
def get_data_dir(app_name: str) -> Path:
"""Get XDG-compliant data directory."""
if os.name == "nt":
base = Path(os.environ.get("LOCALAPPDATA", Path.home()))
else:
base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
data_dir = base / app_name
data_dir.mkdir(parents=True, exist_ok=True)
return data_dir
def get_cache_dir(app_name: str) -> Path:
"""Get XDG-compliant cache directory."""
if os.name == "nt":
base = Path(os.environ.get("LOCALAPPDATA", Path.home())) / "cache"
else:
base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
cache_dir = base / app_name
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dirConfig Init Command
import typer
from pathlib import Path
@app.command()
def init(
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing"),
):
"""Initialize configuration file."""
config_dir = get_config_dir("myapp")
config_file = config_dir / "config.toml"
if config_file.exists() and not force:
typer.echo(f"Config already exists: {config_file}")
if not typer.confirm("Overwrite?"):
raise typer.Abort()
default_config = """
# MyApp Configuration
[server]
host = "localhost"
port = 8080
[logging]
level = "INFO"
format = "json"
[database]
url = "sqlite:///app.db"
""".strip()
config_file.write_text(default_config)
typer.echo(f"Created config: {config_file}")Layered Configuration
from dataclasses import dataclass, field, asdict
import tomllib
from pathlib import Path
import os
@dataclass
class Config:
"""Config with layered loading: defaults < file < env vars < CLI."""
host: str = "localhost"
port: int = 8080
debug: bool = False
@classmethod
def load(
cls,
config_file: Path | None = None,
**cli_overrides,
) -> "Config":
# Start with defaults
config = cls()
# Layer 2: Config file
if config_file and config_file.exists():
with open(config_file, "rb") as f:
file_config = tomllib.load(f)
for key, value in file_config.items():
if hasattr(config, key):
setattr(config, key, value)
# Layer 3: Environment variables
env_mapping = {
"MYAPP_HOST": "host",
"MYAPP_PORT": "port",
"MYAPP_DEBUG": "debug",
}
for env_var, attr in env_mapping.items():
if value := os.environ.get(env_var):
if attr == "port":
value = int(value)
elif attr == "debug":
value = value.lower() in ("true", "1", "yes")
setattr(config, attr, value)
# Layer 4: CLI overrides (highest priority)
for key, value in cli_overrides.items():
if value is not None and hasattr(config, key):
setattr(config, key, value)
return config
@app.command()
def serve(
config: Path = typer.Option(None, "--config", "-c"),
host: str = typer.Option(None, "--host", "-h"),
port: int = typer.Option(None, "--port", "-p"),
debug: bool = typer.Option(None, "--debug", "-d"),
):
"""Start server with layered config."""
cfg = Config.load(
config_file=config,
host=host,
port=port,
debug=debug,
)
typer.echo(f"Starting on {cfg.host}:{cfg.port}")Quick Reference
| Source | Priority | Example |
|---|---|---|
| Defaults | Lowest | host="localhost" |
| Config file | Low | config.toml |
| Env vars | Medium | MYAPP_HOST=0.0.0.0 |
| CLI args | Highest | --host 0.0.0.0 |
| XDG Directory | Purpose | Default |
|---|---|---|
XDG_CONFIG_HOME | Config files | ~/.config |
XDG_DATA_HOME | Persistent data | ~/.local/share |
XDG_CACHE_HOME | Cache | ~/.cache |
Rich Terminal Output
Beautiful CLI output with Rich.
Console Basics
from rich.console import Console
from rich.text import Text
console = Console()
# Basic printing
console.print("Hello, World!")
# With styling
console.print("Hello", style="bold red")
console.print("[bold blue]Bold blue[/bold blue] and [green]green[/green]")
# Print objects (auto-formatting)
console.print({"key": "value", "list": [1, 2, 3]})
# Print to stderr
console.print("Error!", style="red", file=sys.stderr)
# Width control
console.print("Text", width=40, justify="center")Tables
from rich.table import Table
from rich.console import Console
console = Console()
# Basic table
table = Table(title="Users")
table.add_column("ID", style="cyan", justify="right")
table.add_column("Name", style="green")
table.add_column("Email")
table.add_column("Active", justify="center")
table.add_row("1", "Alice", "alice@example.com", "✓")
table.add_row("2", "Bob", "bob@example.com", "✓")
table.add_row("3", "Charlie", "charlie@example.com", "✗")
console.print(table)
# Table with styling
table = Table(
title="Report",
show_header=True,
header_style="bold magenta",
border_style="blue",
box=box.DOUBLE,
)
# Dynamic table from data
def print_users(users: list[dict]):
table = Table()
table.add_column("ID")
table.add_column("Name")
table.add_column("Status")
for user in users:
status = "[green]Active[/green]" if user["active"] else "[red]Inactive[/red]"
table.add_row(str(user["id"]), user["name"], status)
console.print(table)Progress Bars
from rich.progress import (
Progress,
SpinnerColumn,
TextColumn,
BarColumn,
TaskProgressColumn,
TimeRemainingColumn,
track,
)
from rich.console import Console
console = Console()
# Simple progress with track()
for item in track(items, description="Processing..."):
process(item)
# Customizable progress
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
console=console,
) as progress:
task = progress.add_task("Downloading...", total=100)
for i in range(100):
do_work()
progress.update(task, advance=1)
# Multiple tasks
with Progress() as progress:
download_task = progress.add_task("Downloading", total=1000)
process_task = progress.add_task("Processing", total=500)
while not progress.finished:
progress.update(download_task, advance=10)
progress.update(process_task, advance=5)
time.sleep(0.01)
# Indeterminate spinner
with console.status("[bold green]Working...") as status:
while not done:
do_something()
status.update("[bold green]Still working...")Panels and Layout
from rich.panel import Panel
from rich.layout import Layout
from rich.console import Console
console = Console()
# Basic panel
console.print(Panel("Hello, World!", title="Greeting", border_style="green"))
# Panel with rich content
console.print(Panel(
"[bold]Important Message[/bold]\n\n"
"This is a [red]warning[/red] message.",
title="Alert",
subtitle="Action Required",
border_style="red",
))
# Layout for complex UIs
layout = Layout()
layout.split(
Layout(name="header", size=3),
Layout(name="main"),
Layout(name="footer", size=3),
)
layout["header"].update(Panel("My CLI App", style="bold"))
layout["main"].split_row(
Layout(name="left"),
Layout(name="right"),
)
layout["footer"].update(Panel("Press Ctrl+C to exit"))
console.print(layout)Markdown and Syntax
from rich.markdown import Markdown
from rich.syntax import Syntax
from rich.console import Console
console = Console()
# Render markdown
md = Markdown("""
# Title
This is **bold** and *italic*.
- Item 1
- Item 2
print("Hello")
""")
console.print(md)
# Syntax highlighting
code = '''
def hello(name: str) -> str:
"""Say hello."""
return f"Hello, {name}!"
'''
syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
console.print(syntax)
# From file
syntax = Syntax.from_path("script.py", line_numbers=True)
console.print(syntax)Trees
from rich.tree import Tree
from rich.console import Console
console = Console()
tree = Tree("[bold]Project Structure")
src = tree.add("[blue]src/")
src.add("main.py")
src.add("utils.py")
src.add("[blue]models/").add("user.py")
tests = tree.add("[blue]tests/")
tests.add("test_main.py")
console.print(tree)Live Display
from rich.live import Live
from rich.table import Table
from rich.console import Console
import time
console = Console()
def generate_table(count: int) -> Table:
table = Table()
table.add_column("Count")
table.add_column("Status")
table.add_row(str(count), "Processing...")
return table
with Live(generate_table(0), console=console, refresh_per_second=4) as live:
for i in range(100):
time.sleep(0.1)
live.update(generate_table(i))Logging Integration
from rich.logging import RichHandler
import logging
logging.basicConfig(
level="INFO",
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)],
)
logger = logging.getLogger("my_app")
logger.info("Hello, World!")
logger.warning("This is a warning")
logger.error("Something went wrong")Quick Reference
| Component | Usage |
|---|---|
console.print() | Print with styling |
Table() | Tabular data |
track() | Simple progress bar |
Progress() | Custom progress |
Panel() | Bordered content |
Syntax() | Code highlighting |
Markdown() | Render markdown |
Tree() | Hierarchical data |
Live() | Dynamic updates |
| Markup | Effect |
|---|---|
[bold]text[/bold] | Bold |
[red]text[/red] | Red color |
[link=url]text[/link] | Hyperlink |
[dim]text[/dim] | Dimmed |
Advanced Typer Patterns
Modern CLI development patterns with Typer.
Application Structure
import typer
from typing import Optional
from enum import Enum
# Create app with metadata
app = typer.Typer(
name="myapp",
help="My CLI application",
add_completion=True,
no_args_is_help=True, # Show help if no command given
rich_markup_mode="rich", # Enable Rich formatting in help
)
# State object for shared options
class State:
def __init__(self):
self.verbose: bool = False
self.config_path: str = ""
state = State()
@app.callback()
def main(
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
config: str = typer.Option("config.yaml", "--config", "-c", help="Config file"),
):
"""
My awesome CLI application.
Use --help on any command for more info.
"""
state.verbose = verbose
state.config_path = configType-Safe Arguments
from typing import Annotated
from enum import Enum
from pathlib import Path
class OutputFormat(str, Enum):
json = "json"
yaml = "yaml"
table = "table"
@app.command()
def export(
# Required argument
query: Annotated[str, typer.Argument(help="Search query")],
# Optional argument with default
limit: Annotated[int, typer.Argument()] = 10,
# Path validation
output: Annotated[
Path,
typer.Option(
"--output", "-o",
help="Output file path",
exists=False, # Must not exist
file_okay=True,
dir_okay=False,
writable=True,
resolve_path=True,
)
] = None,
# Input file (must exist)
input_file: Annotated[
Path,
typer.Option(
"--input", "-i",
exists=True, # Must exist
readable=True,
)
] = None,
# Enum choices
format: Annotated[
OutputFormat,
typer.Option("--format", "-f", case_sensitive=False)
] = OutputFormat.table,
# Multiple values
tags: Annotated[
list[str],
typer.Option("--tag", "-t", help="Tags to filter")
] = None,
):
"""Export data with various options."""
typer.echo(f"Query: {query}, Format: {format.value}")Interactive Prompts
import typer
@app.command()
def create_user():
"""Create a new user interactively."""
# Text prompt
name = typer.prompt("What's your name?")
# With default
email = typer.prompt("Email", default=f"{name.lower()}@example.com")
# Hidden input (password)
password = typer.prompt("Password", hide_input=True)
# Confirmation
password_confirm = typer.prompt("Confirm password", hide_input=True)
if password != password_confirm:
typer.echo("Passwords don't match!")
raise typer.Abort()
# Yes/No confirmation
if typer.confirm("Create this user?"):
typer.echo(f"Creating user: {name}")
else:
typer.echo("Cancelled")
raise typer.Abort()
# Non-interactive with --yes flag
@app.command()
def delete_all(
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
):
"""Delete all items."""
if not yes:
yes = typer.confirm("Are you sure?")
if yes:
typer.echo("Deleting...")
else:
raise typer.Abort()Context and Dependency Injection
import typer
from typing import Annotated
# Create a context type
class Context:
def __init__(self, db_url: str, debug: bool):
self.db_url = db_url
self.debug = debug
self.db = None
def connect(self):
self.db = create_connection(self.db_url)
# Store in typer context
@app.callback()
def main(
ctx: typer.Context,
db_url: str = typer.Option("sqlite:///app.db", envvar="DATABASE_URL"),
debug: bool = typer.Option(False, "--debug"),
):
"""Initialize application context."""
ctx.obj = Context(db_url=db_url, debug=debug)
ctx.obj.connect()
@app.command()
def query(
ctx: typer.Context,
sql: str,
):
"""Run a SQL query."""
result = ctx.obj.db.execute(sql)
for row in result:
typer.echo(row)Subcommands and Nested Apps
import typer
# Main app
app = typer.Typer()
# Sub-applications
db_app = typer.Typer(help="Database operations")
cache_app = typer.Typer(help="Cache operations")
# Register sub-apps
app.add_typer(db_app, name="db")
app.add_typer(cache_app, name="cache")
@db_app.command("migrate")
def db_migrate():
"""Run database migrations."""
typer.echo("Running migrations...")
@db_app.command("seed")
def db_seed():
"""Seed database with test data."""
typer.echo("Seeding database...")
@cache_app.command("clear")
def cache_clear():
"""Clear cache."""
typer.echo("Clearing cache...")
# Usage:
# myapp db migrate
# myapp db seed
# myapp cache clearAsync Commands
import typer
import asyncio
app = typer.Typer()
async def async_operation():
await asyncio.sleep(1)
return "Done"
@app.command()
def fetch():
"""Fetch data asynchronously."""
result = asyncio.run(async_main())
typer.echo(result)
async def async_main():
results = await asyncio.gather(
async_operation(),
async_operation(),
)
return resultsTesting CLI Apps
from typer.testing import CliRunner
import pytest
runner = CliRunner()
def test_hello():
result = runner.invoke(app, ["hello", "World"])
assert result.exit_code == 0
assert "Hello, World!" in result.stdout
def test_hello_with_options():
result = runner.invoke(app, ["hello", "World", "--count", "3", "--loud"])
assert result.exit_code == 0
assert "HELLO, WORLD!" in result.stdout
assert result.stdout.count("HELLO") == 3
def test_invalid_input():
result = runner.invoke(app, ["process", "nonexistent.txt"])
assert result.exit_code == 1
assert "not found" in result.stdout.lower()
# With environment variables
def test_with_env():
result = runner.invoke(
app,
["connect"],
env={"DATABASE_URL": "sqlite:///test.db"}
)
assert result.exit_code == 0Quick Reference
| Pattern | Syntax |
|---|---|
| App callback | @app.callback() for global options |
| Context | ctx: typer.Context + ctx.obj |
| Envvar | typer.Option(envvar="VAR_NAME") |
| Prompt | typer.prompt("Question") |
| Confirm | typer.confirm("Sure?") |
| Abort | raise typer.Abort() |
| Exit | raise typer.Exit(code=1) |
| Progress | Use Rich track() |
| Decorator | Purpose |
|---|---|
@app.command() | Define a command |
@app.callback() | App initialization |
@sub_app.command() | Subcommand |
{
"schema_version": "2.0",
"meta": {
"generated_at": "2026-01-16T14:00:06.799Z",
"slug": "0xdarkmatter-python-cli-patterns",
"source_url": "https://github.com/0xDarkMatter/claude-mods/tree/main/skills/python-cli-patterns",
"source_ref": "main",
"model": "claude",
"analysis_version": "3.0.0",
"source_type": "community",
"content_hash": "90f2f7659899b02494f25b5329f63a6f0c5e9bca3e99ef5866f70f5491f113c4",
"tree_hash": "bef6e9f5935302f68ab637b51894ff0c67908b497227451f46b2c13591a36f2b"
},
"skill": {
"name": "python-cli-patterns",
"description": "CLI application patterns for Python. Triggers on: cli, command line, typer, click, argparse, terminal, rich, console, terminal ui.",
"summary": "CLI application patterns for Python. Triggers on: cli, command line, typer, click, argparse, termina...",
"icon": "💻",
"version": "1.0.0",
"author": "0xDarkMatter",
"license": "MIT",
"category": "coding",
"tags": [
"python",
"cli",
"typer",
"terminal"
],
"supported_tools": [
"claude",
"codex",
"claude-code"
],
"risk_factors": [
"env_access",
"filesystem"
]
},
"security_audit": {
"risk_level": "low",
"is_blocked": false,
"safe_to_publish": true,
"summary": "Educational documentation skill providing CLI development patterns. Contains legitimate environment variable access for configuration (typer.Option envvar parameters), filesystem access for config file handling (Path.home, XDG directories), and Rich library progress tracking. All patterns are standard CLI development practices aligned with the stated purpose. The 159 static findings are false positives: markdown code examples (backticks for formatting, not execution), standard configuration patterns, and misidentified progress bar code as weak crypto.",
"risk_factor_evidence": [
{
"factor": "env_access",
"evidence": [
{
"file": "references/configuration.md",
"line_start": 16,
"line_end": 31
},
{
"file": "references/typer-patterns.md",
"line_start": 169,
"line_end": 169
}
]
},
{
"factor": "filesystem",
"evidence": [
{
"file": "assets/cli-template.py",
"line_start": 51,
"line_end": 52
},
{
"file": "assets/cli-template.py",
"line_start": 123,
"line_end": 129
},
{
"file": "references/configuration.md",
"line_start": 56,
"line_end": 63
}
]
}
],
"critical_findings": [],
"high_findings": [],
"medium_findings": [],
"low_findings": [],
"dangerous_patterns": [],
"files_scanned": 6,
"total_lines": 1576,
"audit_model": "claude",
"audited_at": "2026-01-16T14:00:06.799Z"
},
"content": {
"user_title": "Build Python CLI Applications",
"value_statement": "Creating command-line interfaces in Python requires understanding argument parsing, output formatting, and configuration management. This skill provides ready-to-use patterns for building professional CLI tools with Typer and Rich libraries.",
"seo_keywords": [
"Python CLI",
"Typer",
"Rich",
"command line",
"terminal",
"argparse",
"CLI patterns",
"Claude",
"Codex",
"Claude Code"
],
"actual_capabilities": [
"Create CLI applications with Typer decorators and argument parsing",
"Build command groups with subcommands for organized commands",
"Format terminal output with Rich tables, panels, and progress bars",
"Handle configuration files and environment variables",
"Implement interactive prompts and user confirmations",
"Test CLI applications using Typer CliRunner"
],
"limitations": [
"Does not execute external commands or system operations",
"Does not make network requests or API calls",
"Focuses on patterns rather than complete applications",
"Python 3.10+ required for all patterns"
],
"use_cases": [
{
"target_user": "Python developers",
"title": "Build Dev Tools",
"description": "Create internal developer tools and automation scripts with professional CLI interfaces"
},
{
"target_user": "DevOps engineers",
"title": "Configure Infrastructure",
"description": "Build configuration management CLIs with layered config files and environment variable support"
},
{
"target_user": "Data engineers",
"title": "Create Data Pipelines",
"description": "Develop data processing CLIs with progress tracking and rich output formatting"
}
],
"prompt_templates": [
{
"title": "Basic CLI Structure",
"scenario": "Starting a new CLI project",
"prompt": "Create a basic Typer CLI application with a main command and one subcommand"
},
{
"title": "Add Rich Output",
"scenario": "Enhancing CLI appearance",
"prompt": "Add a Rich table to display results and a progress bar for long-running operations"
},
{
"title": "Configuration Handling",
"scenario": "Managing app settings",
"prompt": "Implement configuration file loading from ~/.config/myapp/ and environment variable support"
},
{
"title": "Production Template",
"scenario": "Building complete CLI",
"prompt": "Create a production-ready CLI template with state management, error handling, and config subcommands"
}
],
"output_examples": [
{
"input": "Create a CLI command that shows user status in a table",
"output": [
"User table displayed with ID, Name, Email, and Status columns",
"Status column shows green checkmark for active users",
"Header styled with bold magenta text",
"Table includes title and proper column alignment"
]
},
{
"input": "Build a CLI with config subcommands for init, show, and path",
"output": [
"Config init command creates ~/.config/myapp/config.toml",
"Config show command displays current configuration in a panel",
"Config path command prints config file location",
"All commands use typer.confirm for safety"
]
},
{
"input": "Create a file processing CLI with progress bar",
"output": [
"Progress bar tracks file processing status",
"Files are validated for existence and readability",
"Output can be written to file or printed to console",
"Verbose mode logs processing details"
]
}
],
"best_practices": [
"Use type annotations for arguments to enable automatic validation and help text generation",
"Group related commands into subapps for better organization and discoverability",
"Implement --verbose and --config options at the callback level for consistent access across commands"
],
"anti_patterns": [
"Avoid using os.system() or subprocess calls for core functionality - use Typer commands instead",
"Do not hardcode configuration values - use environment variables and config files",
"Avoid mixing business logic with output formatting - separate concerns for testability"
],
"faq": [
{
"question": "What Python version is required?",
"answer": "Python 3.10 or higher is required. Typer uses modern type annotation features."
},
{
"question": "What libraries are needed?",
"answer": "Install typer and rich. Use: pip install typer rich. For complete CLI support, add pydantic-settings."
},
{
"question": "How to handle environment variables?",
"answer": "Use typer.Option(envvar='VAR_NAME') to read environment variables with automatic fallback to defaults."
},
{
"question": "Is user data safe?",
"answer": "This skill provides patterns only. Actual data handling depends on how you implement the CLI."
},
{
"question": "How to test CLI commands?",
"answer": "Use Typer's CliRunner from typer.testing. Invoke commands with argument lists and assert exit codes and output."
},
{
"question": "How does this compare to Click or argparse?",
"answer": "Typer builds on Click with Pythonic syntax. It is more concise than argparse while offering Rich integration for beautiful output."
}
]
},
"file_structure": [
{
"name": "assets",
"type": "dir",
"path": "assets",
"children": [
{
"name": "cli-template.py",
"type": "file",
"path": "assets/cli-template.py",
"lines": 257
}
]
},
{
"name": "references",
"type": "dir",
"path": "references",
"children": [
{
"name": "configuration.md",
"type": "file",
"path": "references/configuration.md",
"lines": 295
},
{
"name": "rich-output.md",
"type": "file",
"path": "references/rich-output.md",
"lines": 294
},
{
"name": "typer-patterns.md",
"type": "file",
"path": "references/typer-patterns.md",
"lines": 304
}
]
},
{
"name": "SKILL.md",
"type": "file",
"path": "SKILL.md",
"lines": 172
}
]
}
Related skills
FAQ
Which libraries does python-cli-patterns use?
Typer for the CLI framework and Rich for tables, progress bars, and colored console output; it requires Python 3.10+.
What is bundled?
A full CLI application template (assets/cli-template.py) plus references for Typer patterns, Rich output, and configuration.