Python argparse and CLI
- Description: Building command-line tools with
argparse,ArgumentParser, positional and optional arguments, types/choices/nargs, actions, mutually exclusive groups, subcommands, and modern alternatives likeclickandtyper - My Notion Note ID: K2A-D1-15
- Created: 2022-11-18
- Updated: 2026-05-11
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Why
argparse - 2.
ArgumentParser - 3.
add_argument - 4. Mutually Exclusive Groups
- 5. Subcommands
- 6.
parse_argsand Using the Result - 7. Alternatives:
clickandtyper
1. Why argparse
- Stdlib CLI parser
- Reads
sys.argv, builds a typed object of parsed values - Auto-generates
-h/--help - C++ has no stdlib equivalent, use
getopt(POSIX), Boost.Program_options, or CLI11
2. ArgumentParser
import argparse
parser = argparse.ArgumentParser(
prog="mytool",
description="What this tool does.",
epilog="Examples: mytool --foo bar",
)
Useful keyword args:
prog=, name shown in help (default:sys.argv[0])description=, short blurb above the arg listepilog=, text below the arg list (good for examples)formatter_class=argparse.RawDescriptionHelpFormatter, preserve newlines in description/epilogfromfile_prefix_chars="@",mytool @args.txtreads args from a file
3. add_argument
3.1 Positional vs Optional
-
Name without leading dashes → positional (required, ordered)
parser.add_argument("input_file") -
Name with leading dashes → optional (flag-style)
parser.add_argument("-o", "--output", help="output path") -
Multiple flag spellings allowed (
"-o", "--output") -
The long form's name (
output) becomes the attribute on the result object
3.2 type, default, choices
parser.add_argument("--workers", type=int, default=4)
parser.add_argument("--ratio", type=float)
parser.add_argument("--config", type=argparse.FileType("r")) # opens the file
parser.add_argument("--level", choices=["debug", "info", "warn", "error"])
parser.add_argument("--mode", type=str.lower) # custom callable
type, any one-arg callable applied to the raw stringargparse.FileType("r")leaks file handles on argparse-level errors, prefer opening files yourself
3.3 nargs
nargs |
Meaning |
|---|---|
| (omitted) | exactly one |
"?" |
zero or one (use const= for "no value given" fallback) |
"*" |
zero or more, list |
"+" |
one or more, list (errors if empty) |
an int N |
exactly N, always a list |
parser.add_argument("paths", nargs="+") # at least one path
parser.add_argument("--tags", nargs="*", default=[]) # optional list of tags
3.4 action
action |
Effect |
|---|---|
"store" (default) |
save the value |
"store_const" |
save const= |
"store_true" |
save True (boolean flag) |
"store_false" |
save False |
"append" |
append to a list (allows repetition: --tag a --tag b) |
"append_const" |
append const= to a list |
"count" |
increment a counter (-vvv gives 3) |
"help", "version" |
print and exit |
"extend" (3.8+) |
extend with multiple values (works with nargs="+") |
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--include", action="append", default=[])
4. Mutually Exclusive Groups
group = parser.add_mutually_exclusive_group()
group.add_argument("--quiet", action="store_true")
group.add_argument("--verbose", action="store_true")
- Enforces at most one of the listed arguments
- Pass
required=Trueto require exactly one
5. Subcommands
For git-style multi-verb interfaces:
parser = argparse.ArgumentParser()
subs = parser.add_subparsers(dest="cmd", required=True)
p_list = subs.add_parser("list", help="list items")
p_list.add_argument("--format", choices=["json", "csv"], default="json")
p_add = subs.add_parser("add", help="add an item")
p_add.add_argument("name")
p_add.add_argument("--quantity", type=int, default=1)
args = parser.parse_args()
match args.cmd:
case "list": cmd_list(args)
case "add": cmd_add(args)
- Each subparser has its own argument set
dest="cmd"puts the chosen subcommand name on the result
Wire handlers with set_defaults(func=handler):
p_list.set_defaults(func=cmd_list)
p_add.set_defaults(func=cmd_add)
args = parser.parse_args()
args.func(args)
6. parse_args and Using the Result
args = parser.parse_args() # uses sys.argv[1:]
args = parser.parse_args(["-v", "input.txt"]) # for testing, pass a list
# Result is argparse.Namespace; attribute names are long form with - → _
args.output
args.dry_run
vars(args) # convert to a dict
- For inter-argument validation (e.g., "either --start or --duration must be given"), parse first then validate
- Call
parser.error("message")to print usage-aware error and exit
7. Alternatives: click and typer
For larger CLIs, decorator-based libraries are more ergonomic:
# click
import click
@click.group()
def cli(): pass
@cli.command()
@click.option("--workers", default=4, show_default=True)
@click.argument("path")
def run(workers, path):
"""Run the thing."""
...
# typer (built on click; uses type hints)
import typer
app = typer.Typer()
@app.command()
def run(path: str, workers: int = 4):
...
click, most populartyper, type-hint-friendly, built on clickargparse, zero dependencies; reach forclick/typerpast a handful of commands