Engineer production-grade Python CLI tools with UV for package management, Ruff for linting, Pyright for strict typing, Typer for commands, and Rich for polished output. Addresses fail-fast patterns, pydantic-settings configuration, modular code organization, and professional UX conventions. Apply when creating admin utilities, data processors, or developer tooling.
Modern patterns for building production-grade Python command-line applications.
Use when building:
Installation:
curl -LsSf https://astral.sh/uv/install.sh | sh # Install UV
uv init my-cli # Initialize project
uv add typer rich # Add dependencies
For complete tool guides, configuration examples, and advanced patterns: See @references/tech-stack.md
# Custom exceptions (core/exceptions.py)
class AppError(Exception):
"""Base exception - let it bubble up."""
pass
# CLI main entry (cli/main.py)
def main() -> None:
"""Main entry point - ONLY catch at top level."""
try:
app()
except AppError as e:
console.print(f"[red]ERROR: {e}[/red]")
raise typer.Exit(code=1) from e
Key Rules:
except Exception: pass or except: return NoneCRITICAL RULE: 500 lines maximum per module
When module approaches 500 lines:
my_cli/
├── pyproject.toml # UV project config
├── .env.example # Template for .env
├── config.yaml # Application defaults
├── Makefile # Development tasks
└── src/
└── my_cli/
├── __init__.py
├── __main__.py # Entry point
├── cli/ # CLI commands
│ ├── __init__.py
│ └── commands.py # Typer app
├── core/ # Business logic
│ ├── __init__.py
│ ├── config.py # pydantic-settings
│ └── exceptions.py
├── db/ # Database layer
│ ├── __init__.py
│ ├── models.py # SQLAlchemy models
│ └── connection.py
└── services/ # External integrations
└── __init__.py
For complete architecture patterns, module organization, and size management: See @references/architecture.md
Pattern: Merge YAML defaults with .env overrides using pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
# Database
db_host: str = "localhost"
db_port: int = 5432
db_name: str
db_user: str
db_password: str # Must be in .env
# API
api_key: str
api_url: str = "https://api.example.com"
settings = Settings() # Fails if db_password or api_key missing
config.yaml (defaults):
db_host: localhost
db_port: 5432
api_url: https://api.example.com
.env (secrets):
DB_PASSWORD=secret
API_KEY=abc123
For complete configuration patterns, validation, and advanced examples: See @references/configuration.md
uv init my-cli
cd my-cli
uv add typer rich pydantic-settings pyyaml
uv add --dev ruff pyright pytest
# src/my_cli/__main__.py
import typer
from rich.console import Console
app = typer.Typer()
console = Console()
@app.command()
def hello(name: str) -> None:
"""Say hello."""
console.print(f"[green]Hello {name}![/green]")
if __name__ == "__main__":
app()
Copy configuration templates:
uv run python -m my_cli hello World
make lint # Run ruff check + format
make type # Run pyright
make test # Run pytest
make check # lint + type + test
make clean # Remove build artifacts
Always run before committing:
make check
Why: Catches type errors, style issues, and test failures early
Build integration: See patterns/make-integration.md - Integrate CLI with Makefiles Multi-method authentication: See patterns/multi-method-auth.md - Support OAuth, TBA, API keys PostgreSQL JSONB: See patterns/postgresql-jsonb.md - Flexible schema with JSONB columns Pydantic flexibility: See patterns/pydantic-flexible.md - Handle dynamic/unknown fields Schema resilience: See patterns/schema-resilience.md - Robust API schema handling Database migrations: See @references/database-migrations.md - Alembic migration patterns
@app.command()
def sync(
config_file: Path = typer.Option("config.yaml"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Sync data from source."""
settings = Settings(_env_file=".env")
if verbose:
console.print(f"[yellow]Connecting to {settings.db_host}[/yellow]")
# Implementation
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
Base = declarative_base()
engine = create_engine(f"postgresql://{settings.db_user}:{settings.db_password}@{settings.db_host}/{settings.db_name}")
Session = sessionmaker(bind=engine)
from rich.progress import track
for item in track(items, description="Processing..."):
process(item)
class AppError(Exception):
"""Base for all app exceptions."""
pass
class ConfigurationError(AppError):
"""Config missing/invalid."""
pass
class DatabaseError(AppError):
"""DB operation failed."""
pass
class ExternalServiceError(AppError):
"""External API failed."""
pass
@app.command()
def process() -> None:
"""Process data."""
if not settings.api_key:
raise ConfigurationError("API_KEY not set in .env")
try:
response = api.fetch_data()
except Exception as e:
raise ExternalServiceError(f"API fetch failed: {e}") from e
# Process response (let exceptions bubble)
[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.12"
reportMissingTypeStubs = true
reportUnknownMemberType = true
from typing import Any
from collections.abc import Sequence
# Function signatures
def process_items(items: Sequence[dict[str, Any]]) -> list[str]:
return [item["name"] for item in items]
# Optional values
from typing import Optional
def get_value(key: str) -> Optional[str]:
return cache.get(key) # Returns str | None
# Type guards
from typing import TypeGuard
def is_string_list(val: list[Any]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
tests/
├── conftest.py # Fixtures
├── test_cli.py # CLI command tests
├── test_config.py # Config loading tests
└── test_services.py # Service integration tests
from typer.testing import CliRunner
runner = CliRunner()
def test_hello_command():
result = runner.invoke(app, ["hello", "World"])
assert result.exit_code == 0
assert "Hello World" in result.stdout
uv build # Creates wheel + sdist
ls dist/ # my_cli-0.1.0-py3-none-any.whl
uv pip install dist/my_cli-0.1.0-py3-none-any.whl
my-cli hello World # Now available as command
For detailed guides: See the references/ directory
For project templates: See the templates/ directory
End of Skill
npx skills add AeyeOps/python-cli-engineering下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer