CertGrid CertGrid
Hands-on Lab·Python Automation for IT

Python Command-Line Arguments with argparse

A script that needs editing to change a hostname is not automation. `argparse` turns a script into a tool: it parses arguments, converts their types, validates them against a set of choices, writes the `--help` output for you, and exits 2 on a bad call so the caller can tell a usage error from a runtime one. This guide covers everything domain 1 needs and the two behaviours that catch people.

Scripting Foundations Guide 4 of 39 Beginner

Written against the versions above. `argparse` has been in the standard library since Python 3.2 and nothing here is version-sensitive. `required=True` on a mutually exclusive group and `required=True` on subparsers both work as shown on **3.14.4**.

Everything on this page runs on the control node. Any machine with Python 3 will do.
Server NameIP AddressOSRolesCPURAMHDD
RUNNER01192.168.0.27Ubuntu 26.04 LTSControl node - every script in this path runs here2 Core4 GB50 GB

Before you start

  1. sys.argv, which is what argparse replaces

    Every Python script already has its arguments, as a list of strings. sys.argv[0] is the script and everything after it is what the caller typed.

    This is the whole of the built-in support, and it is not enough.

    bash Example session
    cat > ~/auto/rawargs.py <<'PY'import sys print("argv     :", sys.argv)print("script   :", sys.argv[0])print("arguments:", sys.argv[1:])PYpython3 ~/auto/rawargs.py --host web01 --dry-run 3argv     : ['/home/sysadmin/auto/rawargs.py', '--host', 'web01', '--dry-run', '3']script   : /home/sysadmin/auto/rawargs.pyarguments: ['--host', 'web01', '--dry-run', '3']

    Expected resultThe full list including the script name, then the arguments on their own.

    Success conditionYou can see what a script receives before anything parses it.

  2. The same thing with argparse

    Two positional arguments, required by their position. Three optional ones, each with a default. One of them converts to int, one is a boolean flag, one is restricted to a set of values.

    bash Example session
    cat > ~/auto/parsed.py <<'PY'import argparse ap = argparse.ArgumentParser(description="Restart a service on a host.")ap.add_argument("host", help="the host to act on")ap.add_argument("service", help="the service to restart")ap.add_argument("--dry-run", action="store_true", help="say what would happen, do nothing")ap.add_argument("--timeout", type=int, default=30, help="seconds to wait (default: %(default)s)")ap.add_argument("--level", choices=["warn", "info", "debug"], default="info")args = ap.parse_args() print("host    ", args.host)print("service ", args.service)print("dry_run ", args.dry_run, type(args.dry_run).__name__)print("timeout ", args.timeout, type(args.timeout).__name__)print("level   ", args.level)PYpython3 ~/auto/parsed.py web01 nginxhost     web01service  nginxdry_run  False booltimeout  30 intlevel    infopython3 ~/auto/parsed.py web01 nginx --dry-run --timeout 5 --level debughost     web01service  nginxdry_run  True booltimeout  5 intlevel    debug

    Expected resultDefaults on the first run; on the second, dry_run True, timeout 5 as an int, and level debug.

    Success conditionYou can define arguments with types and defaults.

  3. The help you did not write

    -h and --help are added automatically, and the text is assembled from the descriptions and defaults already in the code.

    bash Example session
    python3 ~/auto/parsed.py --helpusage: parsed.py [-h] [--dry-run] [--timeout TIMEOUT]                 [--level {warn,info,debug}]                 host service Restart a service on a host. positional arguments:  host                  the host to act on  service               the service to restart options:  -h, --help            show this help message and exit  --dry-run             say what would happen, do nothing  --timeout TIMEOUT     seconds to wait (default: 30)  --level {warn,info,debug}

    Expected resultA usage line, the description, then the positional and optional arguments.

    Success conditionYour script documents itself.

  4. The four ways it rejects a call

    Missing positional, surplus positional, a value of the wrong type, and a value outside choices. All four produce a usage line, a specific message, and the same exit status.

    bash Example session
    python3 ~/auto/parsed.py; echo "exit status: $?"usage: parsed.py [-h] [--dry-run] [--timeout TIMEOUT]                 [--level {warn,info,debug}]                 host serviceparsed.py: error: the following arguments are required: host, serviceexit status: 2python3 ~/auto/parsed.py web01 nginx extra; echo "exit status: $?"usage: parsed.py [-h] [--dry-run] [--timeout TIMEOUT]                 [--level {warn,info,debug}]                 host serviceparsed.py: error: unrecognized arguments: extraexit status: 2python3 ~/auto/parsed.py web01 nginx --timeout soon; echo "exit status: $?"usage: parsed.py [-h] [--dry-run] [--timeout TIMEOUT]                 [--level {warn,info,debug}]                 host serviceparsed.py: error: argument --timeout: invalid int value: 'soon'exit status: 2python3 ~/auto/parsed.py web01 nginx --level shout; echo "exit status: $?"usage: parsed.py [-h] [--dry-run] [--timeout TIMEOUT]                 [--level {warn,info,debug}]                 host serviceparsed.py: error: argument --level: invalid choice: 'shout' (choose from warn, info, debug)exit status: 2

    Expected resultFour different messages, all with exit status 2.

    Success conditionYou can distinguish a usage error from a runtime error by its status.

  5. The hyphen becomes an underscore

    The one detail that catches everybody once. --dry-run on the command line becomes args.dry_run in the code, because a hyphen is not legal in a Python identifier.

    bash Example session
    cat > ~/auto/hyphen.py <<'PY'import argparse ap = argparse.ArgumentParser()ap.add_argument("--dry-run", action="store_true")ap.add_argument("--max-retries", type=int, default=3)args = ap.parse_args(["--dry-run"]) print("the namespace   :", args)print("args.dry_run    :", args.dry_run)print("getattr fallback:", getattr(args, "dry-run", "there is no such attribute"))PYpython3 ~/auto/hyphen.pythe namespace   : Namespace(dry_run=True, max_retries=3)args.dry_run    : Truegetattr fallback: there is no such attribute

    Expected resultNamespace(dry_run=True, max_retries=3) - both names converted.

    Success conditionYou know what attribute name a flag produces.

  6. Several values for one argument

    Three ways, and they are not interchangeable.

    action="append" collects a flag given repeatedly. **nargs="*" takes zero or more values after one flag. nargs="+"** on a positional takes one or more and requires at least one.

    bash Example session
    cat > ~/auto/manyargs.py <<'PY'import argparse ap = argparse.ArgumentParser()ap.add_argument("--host", action="append", default=[], help="repeat for several hosts")ap.add_argument("--tag", nargs="*", default=[], help="zero or more, space separated")ap.add_argument("paths", nargs="+", help="one or more positional paths")args = ap.parse_args() print("hosts:", args.host)print("tags :", args.tag)print("paths:", args.paths)PYpython3 ~/auto/manyargs.py --host web01 --host web02 --tag prod eu /etc/hosts /etc/fstabusage: manyargs.py [-h] [--host HOST] [--tag [TAG ...]] paths [paths ...]manyargs.py: error: the following arguments are required: paths[exit 2]

    Expected resulthosts with two entries, tags with two, paths with two.

    Success conditionYou can accept a list of hosts or files.

  7. Choose one, and you must choose

    A mutually exclusive group rejects two conflicting flags, and required=True rejects neither being given.

    bash Example session
    cat > ~/auto/exclusive.py <<'PY'import argparse ap = argparse.ArgumentParser()group = ap.add_mutually_exclusive_group(required=True)group.add_argument("--start", action="store_true")group.add_argument("--stop", action="store_true")args = ap.parse_args() print("start", args.start, "| stop", args.stop)PYpython3 ~/auto/exclusive.py --startstart True | stop Falsepython3 ~/auto/exclusive.py --start --stop; echo "exit status: $?"usage: exclusive.py [-h] (--start | --stop)exclusive.py: error: argument --stop: not allowed with argument --startexit status: 2python3 ~/auto/exclusive.py; echo "exit status: $?"usage: exclusive.py [-h] (--start | --stop)exclusive.py: error: one of the arguments --start --stop is requiredexit status: 2

    Expected resultThe single flag works; both together is not allowed with argument; neither is one of the arguments ... is required. Both errors exit 2.

    Success conditionYou can enforce a choice without writing the check yourself.

  8. Subcommands, for a script that does several jobs

    add_subparsers gives you git-style commands, each with its own arguments and its own --help. dest="command" records which one was used; required=True means one must be.

    bash Example session
    cat > ~/auto/subs.py <<'PY'import argparse ap = argparse.ArgumentParser(prog="fleet")subs = ap.add_subparsers(dest="command", required=True) check = subs.add_parser("check", help="check disk on a host")check.add_argument("host") deploy = subs.add_parser("deploy", help="deploy a version")deploy.add_argument("host")deploy.add_argument("--version", required=True) args = ap.parse_args()print(args)PYpython3 ~/auto/subs.py --helpusage: fleet [-h] {check,deploy} ... positional arguments:  {check,deploy}    check         check disk on a host    deploy        deploy a version options:  -h, --help      show this help message and exitpython3 ~/auto/subs.py check web01Namespace(command='check', host='web01')python3 ~/auto/subs.py deploy web01 --version 2.1Namespace(command='deploy', host='web01', version='2.1')python3 ~/auto/subs.py deploy web01; echo "exit status: $?"usage: fleet deploy [-h] --version VERSION hostfleet deploy: error: the following arguments are required: --versionexit status: 2

    Expected resultThe top-level help listing both commands, each command parsing its own arguments, and a missing --version rejected with exit 2 and a usage line reading fleet deploy.

    Success conditionYou can build one script that does several distinct jobs.

  9. What domain 1 asks about this

    The recurring shapes:

    "What exit status does a bad argument produce?" - 2.

    "What does --help exit with?" - 0.

    "What attribute does --dry-run set?" - args.dry_run.

    "What type is args.count with type=int?" - int, converted before your code runs.

    "How do you accept several hosts?" - action="append" or nargs="+".

    "How do you require one of two flags?" - a mutually exclusive group with required=True.

    "Does argparse check that a file exists?" - no. type=Path converts but does not test.

    guide 5 is next, and it is the other half of this one.

    bash Example session
    rm -f ~/auto/rawargs.py ~/auto/parsed.py ~/auto/hyphen.py ~/auto/manyargs.py ~/auto/exclusive.py ~/auto/subs.py && ls -A ~/autolabapi.loglabapi.pidlabapi.py

    Expected resultThe scratch directory back to the lab's API fixture.

    Success conditionYour scripts take arguments rather than needing edits.

Troubleshooting

Official sources