writing-python-scripts

SKILL.md

Writing Python Scripts

Single-file scripts use PEP 722 inline metadata for dependencies, executed via uv run --script. All type safety and error handling rules from writing-python-code still apply.


When to Use Single Script

Single Script (PEP 722) Full Project
One task, one file Multiple features
No tests needed Tests required
Templating / generation / automation Application with UI or API
Run directly: ./script.py Run via: uv run poe app
Dependencies in script header Dependencies in pyproject.toml
Under ~500 lines Will grow beyond ~500 lines

Layout

app/
├── script.py             # Self-contained with inline deps
├── template.html         # Jinja2 templates (if generating text)
├── schema.json           # Validation schema (if validating configs)
├── configs/              # Configuration files (if multiple are needed)
├── pyproject.toml        # Tool config only (ruff, basedpyright)
└── .gitignore

Script Template

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.14"
# dependencies = [
#     "typer>=0.12.0",
#     "rusty-results>=1.1.1",
#     # Add as needed:
#     # "jinja2>=3.1.0",       # For text output generation
#     # "pyyaml>=6.0.0",       # For YAML config loading
#     # "jsonschema>=4.20.0",  # For config validation
# ]
# ///

import sys
from pathlib import Path
from typing import Final, TypedDict, Required

import typer
from rusty_results import Result, Ok, Err

# =============================================================================
# Constants & Types
# =============================================================================

TEMPLATE_PATH: Final[Path] = Path(__file__).parent / "template.html"

class ItemConfig(TypedDict):
    name: Required[str]
    # ...

# =============================================================================
# Business Logic
# =============================================================================

def load_config(path: Path) -> Result[ItemConfig, str]: ...
def process_item(config: ItemConfig) -> Result[str, str]: ...

# =============================================================================
# CLI Interface
# =============================================================================

app = typer.Typer(help="Description", add_completion=False)

@app.command()
def main_command() -> None:
    result = do_work()
    if result.is_err:
        typer.echo(f"Error: {result.unwrap_err()}", err=True)
        sys.exit(1)
    typer.echo(result.unwrap())

if __name__ == "__main__":
    app()

Tool Config (pyproject.toml)

No [project] section needed — just ruff + basedpyright config:

[tool.basedpyright]
pythonVersion = "3.14"
typeCheckingMode = "strict"
reportAny = "error"

[tool.ruff]
line-length = 120
target-version = "py314"

[tool.ruff.lint]
extend-select = ["E", "F", "I", "N", "UP", "S", "B", "A", "C4", "RUF"]
ignore = ["S101", "B008", "RUF001"]

CLI Note

Use typer for all scripts with uv. Use argparse only if the script must work without any external dependencies (stdlib-only, no uv).

Weekly Installs
8
First Seen
6 days ago
Installed on
claude-code7
gemini-cli6
github-copilot6
codex6
kimi-cli6
cursor6