argparse
argparse is Python's stdlib CLI parser. It generates --help, validates types, and supports subcommands without third-party dependencies.
Recipe
import argparse
def main():
parser = argparse.ArgumentParser(prog="invoice", description="Manage invoices")
parser.add_argument("--format", choices=["json", "csv"], default="json")
parser.add_argument("id", type=int, nargs="?", help="Invoice ID")
args = parser.parse_args()
print(args)
if __name__ == "__main__":
main()When to reach for this:
- CLI with no external dependencies
- Scripts that need
--helpand type validation - Tools where stdlib-only is a requirement
Working Example
import argparse
import sys
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="deploy",
description="Deploy application to environments",
)
parser.add_argument("--verbose", "-v", action="count", default=0)
sub = parser.add_subparsers(dest="env", required=True)
for name in ("staging", "production"):
p = sub.add_parser(name, help=f"Deploy to {name}")
p.add_argument("--dry-run", action="store_true")
p.add_argument("--tag", required=True, help="Docker image tag")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if args.verbose:
print(f"Deploying {args.tag} to {args.env}", file=sys.stderr)
if args.dry_run:
print("Dry run - no changes made")
return 0
# deploy logic here
return 0
if __name__ == "__main__":
raise SystemExit(main())What this demonstrates:
- Subparsers for
deploy staging/deploy production action="count"for-v,-vvverbosityparse_args(argv)enables testing without subprocessraise SystemExit(main())for proper exit codes
Deep Dive
Common Arguments
| Pattern | Code |
|---|---|
| Flag | action="store_true" |
| Optional value | nargs="?" |
| List | nargs="+" |
| Choices | choices=["a", "b"] |
| Version | action="version", version="1.0" |
Gotchas
- Forgetting
required=Trueon subparsers - no command still runs. Fix:required=True(Python 3.7+). - Not testable - only
parse_args()without argv. Fix: passargvlist in tests. - Help text as afterthought - users confused. Fix:
help=on every argument. - Printing secrets in argparse defaults - visible in
--help. Fix: load secrets from env at runtime. - argparse for complex nested commands - verbose. Fix: consider Click or Typer at 5+ subcommands.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Click | Ergonomic decorators | Stdlib-only required |
| Typer | Type-hint driven | No type hints in project |
| fire | Instant CLI from any object | Production CLI quality needed |
FAQs
argparse or Click?
argparse for simple, zero-dep tools. Click/Typer for complex CLIs.
How do I test argparse?
parser.parse_args(["--format", "csv", "42"]).
Mutually exclusive groups?
group = parser.add_mutually_exclusive_group().
Custom types?
type=my_validator function that converts or raises ArgumentTypeError.
How do I show defaults in help?
argument_default=argparse.SUPPRESS to hide, or leave default to show.
nargs=? meaning?
Zero or one argument. Makes positional optional.
How do I parse config files?
argparse handles CLI only. Load config file separately; CLI overrides.
Boolean --no-flag?
action=argparse.BooleanOptionalAction gives --flag / --no-flag (3.9+).
Epilog for examples?
parser = argparse.ArgumentParser(epilog="Examples: ...", formatter_class=argparse.RawDescriptionHelpFormatter).
How do I deprecate a flag?
Show warning in handler; document in help as deprecated.
Related
- CLI Basics - CLI fundamentals
- Click - decorator alternative
- Config & Environment - config layering
- CLI Best Practices - design rules
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.