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
- Python3.14.4
- Control nodeUbuntu 26.04 LTS
- Managed hostsRHEL 10.0
- requests2.34.2
- paramiko5.0.0
- pytest9.1.1
- PyYAML6.0.3
- boto3 / botocore1.43.78
- TimeAbout 17 min
- Reviewed24 August 2026
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**.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| RUNNER01 | 192.168.0.27 | Ubuntu 26.04 LTS | Control node - every script in this path runs here | 2 Core | 4 GB | 50 GB |
Before you start
- guide 3 -
argparseneeds no install. - You can write a function and a dictionary.
-
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.
-
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 debugExpected resultDefaults on the first run; on the second,
dry_run True,timeout 5as an int, andlevel debug.Success conditionYou can define arguments with types and defaults.
-
The help you did not write
-hand--helpare 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.
-
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: 2Expected resultFour different messages, all with exit status 2.
Success conditionYou can distinguish a usage error from a runtime error by its status.
-
The hyphen becomes an underscore
The one detail that catches everybody once.
--dry-runon the command line becomesargs.dry_runin 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 attributeExpected result
Namespace(dry_run=True, max_retries=3)- both names converted.Success conditionYou know what attribute name a flag produces.
-
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 result
hostswith two entries,tagswith two,pathswith two.Success conditionYou can accept a list of hosts or files.
-
Choose one, and you must choose
A mutually exclusive group rejects two conflicting flags, and
required=Truerejects 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: 2Expected resultThe single flag works; both together is
not allowed with argument; neither isone of the arguments ... is required. Both errors exit 2.Success conditionYou can enforce a choice without writing the check yourself.
-
Subcommands, for a script that does several jobs
add_subparsersgives yougit-style commands, each with its own arguments and its own--help.dest="command"records which one was used;required=Truemeans 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: 2Expected resultThe top-level help listing both commands, each command parsing its own arguments, and a missing
--versionrejected with exit 2 and a usage line readingfleet deploy.Success conditionYou can build one script that does several distinct jobs.
-
What domain 1 asks about this
The recurring shapes:
"What exit status does a bad argument produce?" - 2.
"What does
--helpexit with?" - 0."What attribute does
--dry-runset?" -args.dry_run."What type is
args.countwithtype=int?" -int, converted before your code runs."How do you accept several hosts?" -
action="append"ornargs="+"."How do you require one of two flags?" - a mutually exclusive group with
required=True."Does argparse check that a file exists?" - no.
type=Pathconverts 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.pyExpected resultThe scratch directory back to the lab's API fixture.
Success conditionYour scripts take arguments rather than needing edits.
Troubleshooting
AttributeError: 'Namespace' object has no attribute 'dry-run'.Why: Hyphens become underscores in the attribute name.
Fix:
args.dry_run. Or setdest=explicitly if you want a different name.TypeError: 'NoneType' object is not iterablelooping over an argument.Why: An
appendornargsargument that was never given defaults toNone.Fix:
default=[]. argparse copies it per parse, so the usual mutable-default warning does not apply here.The script accepts a file path that does not exist.
Why:
type=Pathconverts the string; it does not check the filesystem.Fix:Check it yourself and fail with a clear message, or use
type=argparse.FileType('r')if you want argparse to open it.A subcommand script runs with no command and does nothing.
Why:
add_subparsers()withoutrequired=Trueallows no command at all.Fix:
ap.add_subparsers(dest="command", required=True).An argument starting with a hyphen is treated as a flag.
Why: argparse cannot tell
-5from an option.Fix:Put
--before it:script -- -5. Everything after--is positional.