Claude Code for Click: Python CLI Framework — Claude Skills 360 Blog
Blog / AI / Claude Code for Click: Python CLI Framework
AI

Claude Code for Click: Python CLI Framework

Published: January 14, 2028
Read time: 5 min read
By: Claude Skills 360

Click builds Python CLI tools. pip install click. Command: import click. @click.command() def hello(): click.echo("Hello!"). if __name__ == "__main__": hello(). Option: @click.option("--name", default="World", help="Name to greet"). def hello(name): click.echo(f"Hello, {name}!"). Required: @click.option("--email", required=True). Argument: @click.argument("filename"). Type: @click.option("--count", type=int, default=1). click.Choice(["json","yaml","toml"]). click.Path(exists=True, file_okay=True). click.File("r"). click.IntRange(1,100). click.DateTime(formats=["%Y-%m-%d"]). Flag: @click.option("--verbose/--no-verbose", default=False). @click.option("--dry-run", is_flag=True). Multiple: @click.option("--tag", multiple=True)--tag a --tag b("a","b"). Group: @click.group() def cli(): pass. @cli.command() def create(): pass. @cli.command() def delete(): pass. Subcommand: @cli.group() def users(): pass. @users.command() def list(): pass. Context: @click.pass_context def cmd(ctx): ctx.ensure_object(dict); ctx.obj["key"] = val. @click.pass_obj def subcmd(obj): .... invoke_without_command: @click.group(invoke_without_command=True) def cli(ctx): if ctx.invoked_subcommand is None: click.echo("run a subcommand"). Prompt: @click.option("--name", prompt="Your name"). Password: @click.option("--password", prompt=True, hide_input=True, confirmation_prompt=True). Confirm: click.confirm("Delete?", abort=True). progressbar: with click.progressbar(items, label="Processing") as bar: for item in bar: .... Style: click.style("text", fg="green", bold=True). click.secho("msg", fg="red", err=True). Testing: from click.testing import CliRunner. runner = CliRunner(); result = runner.invoke(cmd, ["--name","Alice"]); assert result.exit_code == 0; assert "Alice" in result.output. Claude Code generates Click command groups, typed option decorators, and CliRunner test suites.

CLAUDE.md for Click

## Click Stack
- Version: click >= 8.1 | pip install click
- Command: @click.command() | @click.group() for multi-command CLIs
- Options: @click.option("--flag", type=click.Choice([...]), required=True)
- Arguments: @click.argument("PATH", type=click.Path(exists=True))
- Context: @click.pass_context / @click.pass_obj for shared state across commands
- Test: CliRunner().invoke(cmd, args) — captures output and exit_code
- Echo: click.echo() for stdout | click.secho(fg="red", err=True) for stderr

Click CLI Pipeline

#!/usr/bin/env python3
# app/cli.py — Click CLI with groups, typed options, context, and tests
from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Optional

import click
from click.testing import CliRunner


# ─────────────────────────────────────────────────────────────────────────────
# Shared state — passed via context object
# ─────────────────────────────────────────────────────────────────────────────

class Config:
    def __init__(self, verbose: bool = False, output_format: str = "text") -> None:
        self.verbose       = verbose
        self.output_format = output_format
        self._data: list[dict] = [
            {"id": 1, "email": "[email protected]", "role": "admin"},
            {"id": 2, "email": "[email protected]",   "role": "user"},
        ]

    def emit(self, data: object) -> None:
        if self.output_format == "json":
            click.echo(json.dumps(data, indent=2))
        else:
            click.echo(str(data))

    def vlog(self, message: str) -> None:
        if self.verbose:
            click.secho(f"[DEBUG] {message}", fg="cyan", err=True)


# ─────────────────────────────────────────────────────────────────────────────
# Root group
# ─────────────────────────────────────────────────────────────────────────────

@click.group()
@click.option("--verbose", "-v", is_flag=True, default=False,
              help="Enable verbose output.")
@click.option("--format", "output_format",
              type=click.Choice(["text", "json"], case_sensitive=False),
              default="text", show_default=True,
              help="Output format.")
@click.version_option(version="1.0.0", prog_name="myapp")
@click.pass_context
def cli(ctx: click.Context, verbose: bool, output_format: str) -> None:
    """myapp — example CLI built with Click."""
    ctx.ensure_object(dict)
    ctx.obj = Config(verbose=verbose, output_format=output_format)


# ─────────────────────────────────────────────────────────────────────────────
# users sub-group
# ─────────────────────────────────────────────────────────────────────────────

@cli.group()
def users() -> None:
    """Manage users."""


@users.command("list")
@click.option("--role", type=click.Choice(["user", "moderator", "admin"]),
              default=None, help="Filter by role.")
@click.option("--limit", type=click.IntRange(1, 1000), default=20,
              show_default=True, help="Maximum results.")
@click.pass_obj
def users_list(cfg: Config, role: Optional[str], limit: int) -> None:
    """List users, optionally filtered by role."""
    cfg.vlog(f"listing users role={role} limit={limit}")
    data = cfg._data
    if role:
        data = [u for u in data if u["role"] == role]
    cfg.emit(data[:limit])


@users.command("get")
@click.argument("user_id", type=int)
@click.pass_obj
def users_get(cfg: Config, user_id: int) -> None:
    """Get a single user by ID."""
    cfg.vlog(f"fetching user_id={user_id}")
    user = next((u for u in cfg._data if u["id"] == user_id), None)
    if user is None:
        click.secho(f"Error: user {user_id} not found.", fg="red", err=True)
        raise SystemExit(1)
    cfg.emit(user)


@users.command("create")
@click.option("--email",  required=True, help="User email address.")
@click.option("--role",   type=click.Choice(["user", "moderator", "admin"]),
              default="user", show_default=True, help="User role.")
@click.option("--password", prompt=True, hide_input=True,
              confirmation_prompt=True, help="Password (prompted if omitted).")
@click.pass_obj
def users_create(cfg: Config, email: str, role: str, password: str) -> None:
    """Create a new user."""
    cfg.vlog(f"creating user email={email} role={role}")
    new_id = max(u["id"] for u in cfg._data) + 1
    user   = {"id": new_id, "email": email, "role": role}
    cfg._data.append(user)
    click.secho(f"Created user #{new_id}: {email}", fg="green")
    cfg.emit(user)


@users.command("delete")
@click.argument("user_id", type=int)
@click.option("--yes", "-y", is_flag=True, default=False,
              help="Skip confirmation prompt.")
@click.pass_obj
def users_delete(cfg: Config, user_id: int, yes: bool) -> None:
    """Delete a user by ID."""
    user = next((u for u in cfg._data if u["id"] == user_id), None)
    if user is None:
        click.secho(f"Error: user {user_id} not found.", fg="red", err=True)
        raise SystemExit(1)
    if not yes:
        click.confirm(f"Delete user #{user_id} ({user['email']})?", abort=True)
    cfg._data = [u for u in cfg._data if u["id"] != user_id]
    click.secho(f"Deleted user #{user_id}.", fg="yellow")


# ─────────────────────────────────────────────────────────────────────────────
# File processing command — click.Path + click.File
# ─────────────────────────────────────────────────────────────────────────────

@cli.command("import")
@click.argument("input_file", type=click.Path(exists=True, dir_okay=False))
@click.option("--output", "-o",
              type=click.Path(dir_okay=False, writable=True),
              default="-",
              help="Output file path (default: stdout).")
@click.option("--dry-run", is_flag=True, default=False,
              help="Validate without writing output.")
@click.pass_obj
def import_data(cfg: Config, input_file: str, output: str, dry_run: bool) -> None:
    """Import users from a JSON file."""
    cfg.vlog(f"importing from {input_file}")
    with open(input_file) as f:
        records = json.load(f)

    if not isinstance(records, list):
        click.secho("Error: file must contain a JSON array.", fg="red", err=True)
        raise SystemExit(1)

    click.echo(f"Found {len(records)} record(s).")
    if dry_run:
        click.secho("Dry run — no changes written.", fg="yellow")
        return

    out_data = json.dumps(records, indent=2)
    if output == "-":
        click.echo(out_data)
    else:
        Path(output).write_text(out_data)
        click.secho(f"Written to {output}", fg="green")


# ─────────────────────────────────────────────────────────────────────────────
# progressbar demo
# ─────────────────────────────────────────────────────────────────────────────

@cli.command("process")
@click.argument("count", type=click.IntRange(1, 10_000), default=5)
@click.pass_obj
def process_items(cfg: Config, count: int) -> None:
    """Process N items with a progress bar."""
    items = range(count)
    with click.progressbar(items, label=f"Processing {count} items") as bar:
        results = []
        for i in bar:
            results.append(i * 2)
    click.secho(f"Done. Processed {len(results)} items.", fg="green")


# ─────────────────────────────────────────────────────────────────────────────
# Tests — CliRunner
# ─────────────────────────────────────────────────────────────────────────────

def run_tests() -> None:
    runner = CliRunner()

    # List all users
    result = runner.invoke(cli, ["users", "list"])
    assert result.exit_code == 0, result.output
    assert "[email protected]" in result.output
    print("PASS: users list")

    # List with JSON format
    result = runner.invoke(cli, ["--format", "json", "users", "list"])
    assert result.exit_code == 0
    data = json.loads(result.output)
    assert len(data) == 2
    print("PASS: users list --format json")

    # Filter by role
    result = runner.invoke(cli, ["users", "list", "--role", "admin"])
    assert result.exit_code == 0
    assert "alice" in result.output
    assert "bob" not in result.output
    print("PASS: users list --role admin")

    # Get existing user
    result = runner.invoke(cli, ["users", "get", "1"])
    assert result.exit_code == 0
    assert "alice" in result.output
    print("PASS: users get 1")

    # Get missing user
    result = runner.invoke(cli, ["users", "get", "999"])
    assert result.exit_code == 1
    print("PASS: users get 999 (not found)")

    # Create user — CliRunner supplies password via input
    result = runner.invoke(cli, ["users", "create", "--email", "[email protected]"],
                           input="pass1234\npass1234\n")
    assert result.exit_code == 0, result.output
    assert "[email protected]" in result.output
    print("PASS: users create")

    # Delete with --yes to skip confirmation
    result = runner.invoke(cli, ["users", "delete", "1", "--yes"])
    assert result.exit_code == 0
    assert "Deleted" in result.output
    print("PASS: users delete --yes")

    # Import from temp file
    import tempfile, os
    with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
        json.dump([{"email": "[email protected]", "role": "user"}], f)
        tmp = f.name
    try:
        result = runner.invoke(cli, ["import", tmp, "--dry-run"])
        assert result.exit_code == 0
        assert "1 record" in result.output
        print("PASS: import --dry-run")
    finally:
        os.unlink(tmp)

    # --version
    result = runner.invoke(cli, ["--version"])
    assert result.exit_code == 0
    assert "1.0.0" in result.output
    print("PASS: --version")

    print("\nAll Click tests passed.")


if __name__ == "__main__":
    if "--test" in sys.argv:
        run_tests()
    else:
        cli()

For the argparse alternative — argparse requires building an ArgumentParser, calling add_argument() for each parameter, calling parse_args(), and manually routing to subcommand functions, while Click’s @click.command() and @click.option() decorators attach argument definitions directly to the function they configure — the function signature documents and validates the CLI in one place, @click.pass_obj propagates shared state without passing a namespace manually, and CliRunner().invoke(cmd, args) tests the full CLI without spawning a subprocess. For the typer alternative — Typer builds on Click and infers option names and types from Python function annotations (def cmd(name: str, count: int = 1)), which reduces boilerplate for simple CLIs, while Click’s explicit @click.option("--name", type=click.Choice([...]), callback=fn) gives fine-grained control over eager options, multi-value options, environment variable overrides, and custom type converters — Click is the right choice when you need that level of precision. The Claude Skills 360 bundle includes Click skill sets covering @click.command and @click.group, @click.option with Choice/Path/File/IntRange/DateTime types, required/prompt/hide_input/is_flag/multiple options, @click.argument positional parameters, @click.pass_context and @click.pass_obj shared state, invoke_without_command, click.confirm and click.progressbar, click.secho with color and err=True, CliRunner for test automation, and multi-level command group nesting. Start with the free tier to try CLI framework code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free