argparse is Python’s stdlib CLI argument parser. import argparse. Basic: parser = argparse.ArgumentParser(description="My tool"); parser.add_argument("input", help="Input file"); args = parser.parse_args(). Optional: parser.add_argument("--verbose","-v", action="store_true"). Type: parser.add_argument("--port", type=int, default=8080). Choices: parser.add_argument("--env", choices=["dev","prod"]). nargs: "?" zero-or-one, "*" zero-or-more, "+" one-or-more, N exactly N. append: parser.add_argument("--tag", action="append") — repeated flags. count: parser.add_argument("-v", action="count", default=0) — -vvv → 3. required: parser.add_argument("--token", required=True). metavar: parser.add_argument("--out", metavar="FILE"). dest: parser.add_argument("--dry-run", dest="dry_run"). Subparsers: subs = parser.add_subparsers(dest="cmd"); push = subs.add_parser("push"); push.add_argument("remote"). Mutually exclusive: g = parser.add_mutually_exclusive_group(); g.add_argument("--json"); g.add_argument("--yaml"). Argument group: grp = parser.add_argument_group("Database"). Formatter: formatter_class=argparse.ArgumentDefaultsHelpFormatter. FileType: parser.add_argument("--config", type=argparse.FileType("r")). parse_known_args: args, rest = parser.parse_known_args(). set_defaults: parser.set_defaults(func=handler). Namespace: args.input; args.verbose; vars(args). parser.error("msg") — exit with usage. parser.add_argument("--config", default=argparse.SUPPRESS). Claude Code generates git-style CLIs, daemon launchers, ETL runner scripts, and tool wrappers.
CLAUDE.md for argparse
## argparse Stack
- Stdlib: import argparse, sys
- Parser: parser = argparse.ArgumentParser(description="...", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- Sub-commands: subs = parser.add_subparsers(dest="command", required=True); cmd = subs.add_parser("name")
- Dispatch: parser.set_defaults(func=handler) on each sub-parser; args.func(args)
- Types: type=int|float|Path|argparse.FileType("r") | custom validator function
- Groups: parser.add_mutually_exclusive_group(required=True) | parser.add_argument_group("section")
argparse CLI Pipeline
# app/cli.py — ArgumentParser, subparsers, dispatch, validators, config overlay
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Callable, Sequence
# ─────────────────────────────────────────────────────────────────────────────
# 1. Argument type validators
# ─────────────────────────────────────────────────────────────────────────────
def positive_int(value: str) -> int:
"""
Argument type: parse as int, reject if <= 0.
Example:
parser.add_argument("--workers", type=positive_int, default=4)
"""
n = int(value)
if n <= 0:
raise argparse.ArgumentTypeError(f"must be a positive integer, got {value!r}")
return n
def port_number(value: str) -> int:
"""
Argument type: validate as TCP port (1–65535).
Example:
parser.add_argument("--port", type=port_number, default=8080)
"""
n = int(value)
if not (1 <= n <= 65535):
raise argparse.ArgumentTypeError(f"port must be 1–65535, got {n}")
return n
def existing_path(value: str) -> Path:
"""
Argument type: path that must already exist.
Example:
parser.add_argument("config", type=existing_path)
"""
p = Path(value)
if not p.exists():
raise argparse.ArgumentTypeError(f"path does not exist: {value!r}")
return p
def writable_dir(value: str) -> Path:
"""
Argument type: directory; created if it does not exist.
Example:
parser.add_argument("--output-dir", type=writable_dir, default="./output")
"""
p = Path(value)
p.mkdir(parents=True, exist_ok=True)
return p
def key_value_pair(value: str) -> tuple[str, str]:
"""
Argument type for KEY=VALUE pairs.
Example:
parser.add_argument("--env", type=key_value_pair, action="append")
# --env FOO=bar --env BAZ=qux → [("FOO","bar"),("BAZ","qux")]
"""
if "=" not in value:
raise argparse.ArgumentTypeError(f"expected KEY=VALUE, got {value!r}")
k, _, v = value.partition("=")
return k.strip(), v.strip()
# ─────────────────────────────────────────────────────────────────────────────
# 2. Base parser factory
# ─────────────────────────────────────────────────────────────────────────────
def base_parser(
description: str = "",
epilog: str = "",
**kwargs,
) -> argparse.ArgumentParser:
"""
Create a parser with sensible defaults: auto-helps, default display.
Example:
parser = base_parser("ETL runner", epilog="Use 'cmd --help' for more.")
parser.add_argument("--dry-run", action="store_true")
"""
return argparse.ArgumentParser(
description=description,
epilog=epilog,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
**kwargs,
)
def add_common_args(parser: argparse.ArgumentParser) -> None:
"""
Add standard --verbose/-v and --log-level flags to any parser.
Example:
add_common_args(parser)
add_common_args(sub_parser)
"""
group = parser.add_argument_group("logging")
group.add_argument(
"--verbose", "-v",
action="count",
default=0,
help="verbosity level (-v INFO, -vv DEBUG)",
)
group.add_argument(
"--log-level",
choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL"],
default=None,
help="set log level explicitly (overrides --verbose)",
)
def add_config_args(parser: argparse.ArgumentParser) -> None:
"""
Add --config FILE and --set KEY=VALUE arguments.
Example:
add_config_args(parser)
# --config myapp.json --set debug=true --set port=9000
"""
parser.add_argument(
"--config",
type=existing_path,
metavar="FILE",
help="JSON config file to load",
)
parser.add_argument(
"--set",
type=key_value_pair,
action="append",
metavar="KEY=VALUE",
dest="overrides",
default=[],
help="override config key (repeatable)",
)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Subcommand dispatcher
# ─────────────────────────────────────────────────────────────────────────────
class CLI:
"""
Subcommand-based CLI with automatic dispatch.
Example:
cli = CLI("myapp", description="My application")
@cli.command("serve", help="Start the HTTP server")
def serve_cmd(args):
parser.add_argument("--port", type=port_number, default=8080)
...
cli.run()
"""
def __init__(self, prog: str, description: str = "") -> None:
self.parser = base_parser(description, prog=prog)
add_common_args(self.parser)
add_config_args(self.parser)
self._subs = self.parser.add_subparsers(dest="command", metavar="COMMAND")
self._subs.required = True
self._handlers: dict[str, Callable] = {}
def command(
self,
name: str,
help: str = "",
setup: Callable[[argparse.ArgumentParser], None] | None = None,
):
"""
Decorator to register a subcommand.
Example:
@cli.command("ingest", help="Ingest data from source")
def ingest(args):
run_ingest(path=args.path, limit=args.limit)
"""
def decorator(fn: Callable) -> Callable:
sub = self._subs.add_parser(
name,
help=help,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
if setup:
setup(sub)
sub.set_defaults(_handler=fn)
self._handlers[name] = fn
return fn
return decorator
def run(self, argv: Sequence[str] | None = None) -> int:
args = self.parser.parse_args(argv)
_configure_logging(args)
config = _load_config(args)
args.config_data = config
handler = getattr(args, "_handler", None)
if handler is None:
self.parser.print_help()
return 1
try:
result = handler(args)
return 0 if result is None else int(result)
except KeyboardInterrupt:
return 130
except Exception as exc:
self.parser.error(str(exc))
return 1
def _configure_logging(args: argparse.Namespace) -> None:
import logging
level = getattr(args, "log_level", None) or None
verbosity = getattr(args, "verbose", 0)
if level:
logging.basicConfig(level=level)
elif verbosity >= 2:
logging.basicConfig(level=logging.DEBUG)
elif verbosity >= 1:
logging.basicConfig(level=logging.INFO)
else:
logging.basicConfig(level=logging.WARNING)
def _load_config(args: argparse.Namespace) -> dict:
config: dict = {}
cfg_path = getattr(args, "config", None)
if cfg_path and Path(cfg_path).exists():
config = json.loads(Path(cfg_path).read_text())
for key, value in getattr(args, "overrides", []):
config[key] = value
return config
# ─────────────────────────────────────────────────────────────────────────────
# 4. Demo CLI — git-style with subcommands
# ─────────────────────────────────────────────────────────────────────────────
def build_demo_parser() -> argparse.ArgumentParser:
"""
Build an example CLI with serve, process, and export subcommands.
Example:
parser = build_demo_parser()
args = parser.parse_args(["serve", "--port", "9000"])
# args.command == "serve", args.port == 9000
"""
parser = base_parser(
"data-tool",
description="Example data pipeline CLI",
epilog="Use 'data-tool COMMAND --help' for subcommand help.",
)
add_common_args(parser)
add_config_args(parser)
subs = parser.add_subparsers(dest="command", metavar="COMMAND")
subs.required = True
# ── serve ──────────────────────────────────────────────────────────────
serve = subs.add_parser(
"serve",
help="Start the API server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
serve.add_argument("--host", default="0.0.0.0", help="Bind address")
serve.add_argument("--port", type=port_number, default=8080, help="TCP port")
serve.add_argument("--workers", type=positive_int, default=2, help="Worker count")
serve.add_argument("--reload", action="store_true", help="Auto-reload on change")
# ── process ────────────────────────────────────────────────────────────
process = subs.add_parser(
"process",
help="Process an input file",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
process.add_argument("input", type=existing_path, help="Input file path")
process.add_argument("--output", type=writable_dir, default="./output")
process.add_argument("--dry-run", action="store_true", dest="dry_run")
process.add_argument("--limit", type=positive_int, metavar="N", help="Max rows")
process.add_argument(
"--format",
choices=["csv","parquet","json"],
default="csv",
help="Output format",
)
# Mutually exclusive: --all vs --recent
mode = process.add_mutually_exclusive_group()
mode.add_argument("--all", action="store_true", help="Process all records")
mode.add_argument("--recent", type=positive_int, metavar="DAYS",
help="Process records from last N days")
# ── export ─────────────────────────────────────────────────────────────
export = subs.add_parser(
"export",
help="Export data to destination",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
export.add_argument("destination", help="Export destination URL or path")
export.add_argument("--tag", action="append", default=[], help="Filter tags (repeatable)")
export.add_argument("--since", metavar="DATE", help="ISO date lower bound")
export.add_argument("--until", metavar="DATE", help="ISO date upper bound")
export.add_argument(
"--compress",
choices=["none","gzip","zstd"],
default="none",
)
return parser
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
parser = build_demo_parser()
print("=== argparse demo ===")
# 1. serve subcommand
print("\n--- serve args ---")
args = parser.parse_args(["serve", "--port", "9000", "--workers", "4", "--reload"])
print(f" command={args.command} port={args.port} workers={args.workers} reload={args.reload}")
# 2. process subcommand
import tempfile
with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
f.write(b"a,b\n1,2\n")
tmp = f.name
print("\n--- process args ---")
args2 = parser.parse_args([
"process", tmp,
"--format", "parquet",
"--limit", "1000",
"--dry-run",
"--recent", "7",
])
print(f" command={args2.command} format={args2.format}")
print(f" limit={args2.limit} dry_run={args2.dry_run} recent={args2.recent}")
Path(tmp).unlink(missing_ok=True)
# 3. export subcommand
print("\n--- export args ---")
args3 = parser.parse_args([
"export", "s3://my-bucket/data",
"--tag", "daily",
"--tag", "prod",
"--compress", "gzip",
])
print(f" destination={args3.destination} tags={args3.tag} compress={args3.compress}")
# 4. Namespace → dict
print("\n--- vars(args) ---")
d = vars(args)
print(f" {d}")
# 5. parse_known_args
print("\n--- parse_known_args ---")
known, extra = parser.parse_known_args(["serve", "--port", "8080", "--unknown-flag"])
print(f" known.port={known.port} extra={extra}")
print("\n=== done ===")
For the click alternative — click (PyPI) builds CLIs using decorators: @click.command, @click.option, @click.argument, with automatic ANSI color support, prompting, password masking, file handling, and plugin systems via click.Group; Python’s stdlib argparse requires more setup boilerplate but has zero dependencies, native Python integration, and is always available — use click when building user-facing tools that need interactive prompting, rich help formatting, or plugin architectures, argparse for library CLIs, simple internal tooling, or anywhere you must avoid external dependencies. For the typer alternative — typer (PyPI) wraps click to generate CLI argument definitions from Python type annotations and docstrings, reducing @option boilerplate to just function signatures; argparse uses explicit add_argument() calls but integrates cleanly with test suites via parser.parse_args([...]) without subprocess overhead — use typer when you want typed function signatures to drive your CLI with minimal code, argparse when you need fine-grained control over help formatting, subcommand dispatch, or want the stdlib-only solution. The Claude Skills 360 bundle includes argparse skill sets covering positive_int()/port_number()/existing_path()/writable_dir()/key_value_pair() type validators, base_parser()/add_common_args()/add_config_args() parser factories, CLI class with subcommand dispatch, build_demo_parser() with serve/process/export subcommands, mutually exclusive groups, and parse_known_args() partial parsing. Start with the free tier to try CLI building and argparse command pipeline code generation.