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 like click and typer
  • 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

  • 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 list
  • epilog=, text below the arg list (good for examples)
  • formatter_class=argparse.RawDescriptionHelpFormatter, preserve newlines in description/epilog
  • fromfile_prefix_chars="@", mytool @args.txt reads 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 string
  • argparse.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=True to 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 popular
  • typer, type-hint-friendly, built on click
  • argparse, zero dependencies; reach for click/typer past a handful of commands