Beyond the basicsChapter 97 of 114
Command-line Arguments
Turn a script into a proper tool with argparse.
sys.argv is the raw version
Everything typed after the script name, as strings:
# greet.py
import sys
print(sys.argv)Output, from a real run elsewhere
['greet.py', 'Ada', '--loud']
It works, and it leaves you to do everything: check the count, convert types, handle flags in any order, and write the help text. argparse does all of that.
A parser in four lines
import argparse
parser = argparse.ArgumentParser(description="Greet someone.")
parser.add_argument("name", help="who to greet")
args = parser.parse_args(["Ada"])
print(args.name)Output
Ada
Passing a list to parse_args is how you test a parser, and how these examples run. In a real script you call parser.parse_args() with nothing and it reads sys.argv.
Positional and optional
A name without dashes is required and positional. With dashes it is optional:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("name")
parser.add_argument("--greeting", default="Hello")
parser.add_argument("--times", type=int, default=1)
args = parser.parse_args(["Ada", "--times", "2"])
for _ in range(args.times):
print(f"{args.greeting}, {args.name}")Output
Hello, Ada Hello, Ada
type=int converts for you, and rejects anything that will not convert — so the rest of your program never sees a bad value.
Flags
action="store_true" makes an option that takes no value:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--loud", action="store_true")
parser.add_argument("-n", "--dry-run", action="store_true")
args = parser.parse_args(["--loud"])
print(args.loud, args.dry_run)
args = parser.parse_args(["-n"])
print(args.loud, args.dry_run)Output
True False False True
Note --dry-run becomes args.dry_run. Dashes turn into underscores.
Choices, counts and lists
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["fast", "safe"], default="safe")
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument("--tag", action="append", default=[])
parser.add_argument("files", nargs="*")
args = parser.parse_args(["--mode", "fast", "-vv", "--tag", "a", "--tag", "b", "x.txt", "y.txt"])
print(args.mode, args.verbose, args.tag, args.files)Output
fast 2 ['a', 'b'] ['x.txt', 'y.txt']
choices validates for you, count gives the familiar -v/-vv verbosity, append collects repeats, and nargs="*" takes whatever is left.
It validates, and it explains
A bad value produces a message and a non-zero exit, without you writing any of it:
import argparse
import io
from contextlib import redirect_stderr
parser = argparse.ArgumentParser(prog="tool")
parser.add_argument("--mode", choices=["fast", "safe"])
complaint = io.StringIO()
try:
with redirect_stderr(complaint):
parser.parse_args(["--mode", "sideways"])
except SystemExit as problem:
print("exited with", problem.code)
message = complaint.getvalue()
print("invalid choice" in message, "sideways" in message)Output
exited with 2 True True
The exact wording shifts between Python versions, so the example checks the message rather than quoting it. What matters is that you wrote no error handling and still got a precise complaint plus a non-zero exit.
argparse calls sys.exit(2) on a usage error, which is the conventional code. The message goes to stderr.
Help comes free
import argparse
parser = argparse.ArgumentParser(prog="greet", description="Greet someone.")
parser.add_argument("name", help="who to greet")
parser.add_argument("--times", type=int, default=1, help="how many times")
print(parser.format_help().strip())Output
usage: greet [-h] [--times TIMES] name Greet someone. positional arguments: name who to greet options: -h, --help show this help message and exit --times TIMES how many times
-h was added for you, and the text is generated from the help= strings. This is the main reason to use argparse over hand-rolling.
Subcommands
For a tool with several verbs, like git commit and git push:
import argparse
parser = argparse.ArgumentParser(prog="tool")
subs = parser.add_subparsers(dest="command", required=True)
add = subs.add_parser("add", help="add an item")
add.add_argument("item")
remove = subs.add_parser("remove", help="remove an item")
remove.add_argument("item")
remove.add_argument("--force", action="store_true")
args = parser.parse_args(["remove", "notes.txt", "--force"])
print(args.command, args.item, args.force)Output
remove notes.txt True
Each subcommand gets its own arguments and its own help.
The usual shape
import argparse
def main():
parser = argparse.ArgumentParser(description="What this tool does.")
parser.add_argument("path")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
if args.dry_run:
print("would process", args.path)
return 0
print("processing", args.path)
return 0
if __name__ == "__main__":
raise SystemExit(main())Putting the work in main() and returning an exit code keeps the script importable and testable, and raise SystemExit(main()) passes the code to the shell.
Test yourself
2 questionsWhat does action="store_true" give you?
Show the answer
A flag that takes no value — And --dry-run becomes args.dry_run, because dashes turn into underscores.
What does argparse do on a usage error?
Show the answer
Prints a message to stderr and exits with code 2 — You wrote no error handling and still got a precise complaint plus the conventional exit code.
Debugging
Read a traceback, then find the problem with a debugger instead of guessing.