Python argparse: Build Command-Line Tools with Examples
Scripts That Take Instructions
Professional tools are controlled from the command line: git commit -m "message", pip install --upgrade numpy, python -m http.server 8000. Your own Python scripts can work the same way, taking file names, numbers and options when they start, instead of asking questions with input() or needing their code edited. Python's standard library includes argparse to build such command-line interfaces, complete with automatic help messages and error checking. This tutorial covers Python argparse step by step: sys.argv, positional and optional arguments, types, defaults, choices, flags, multiple values, mutually exclusive options, subcommands and a complete grade-calculator tool. It builds on the tutorials on Python functions and command-line basics.
The Raw Material: sys.argv
When you run python grades.py results.csv --top 3, Python stores the words in the list sys.argv: ["grades.py", "results.csv", "--top", "3"]. You could pick these apart by hand, but checking types, handling missing values, supporting options in any order and writing help text quickly becomes tedious. argparse does all of this for you.
A First Parser
import argparse
parser = argparse.ArgumentParser(prog="greet", description="Greet someone by name.")
parser.add_argument("name", help="the person to greet")
parser.add_argument("--times", type=int, default=1, help="how many times to greet (default: 1)")
args = parser.parse_args(["Ada", "--times", "2"])
for _ in range(args.times):
print(f"Hello, {args.name}!")
print(args)
Output:
Hello, Ada! Hello, Ada! Namespace(name='Ada', times=2)
In a real script you call parser.parse_args() with no list, and it reads sys.argv automatically. Passing a list, as here, is the standard way to test a parser without the command line. parse_args returns a Namespace object, with each argument as an attribute.
Positional and Optional Arguments
- Positional arguments (like
name) are identified by their position and are required. - Optional arguments start with
--(or a single-for a short form) and can appear in any order. They are optional unless you passrequired=True.
A dash inside an option's name becomes an underscore in the attribute name, so --pass-mark is read as args.pass_mark.
Automatic Help
Every parser gets -h and --help options for free, generated from the descriptions you supply:
print(parser.format_help())
Output:
usage: greet [-h] [--times TIMES] name Greet someone by name. positional arguments: name the person to greet options: -h, --help show this help message and exit --times TIMES how many times to greet (default: 1)
Write a short, clear help string for every argument. It is your tool's documentation.
Error Messages for Free
If the user gives a missing or invalid argument, argparse prints the usage line and a clear error message, then exits with status code 2.
import contextlib, io
def try_parse(argv):
err = io.StringIO()
try:
with contextlib.redirect_stderr(err):
return parser.parse_args(argv)
except SystemExit as e:
print("exit code", e.code, "|", err.getvalue().strip().splitlines()[-1])
try_parse([])
try_parse(["Ada", "--times", "two"])
Output:
exit code 2 | greet: error: the following arguments are required: name exit code 2 | greet: error: argument --times: invalid int value: 'two'
The helper captures the error so it can be shown here. In a normal script, argparse prints the message to the terminal and stops the program.
Types, Defaults, Choices and Flags
p = argparse.ArgumentParser(prog="report")
p.add_argument("--level", type=int, choices=[100, 200, 300, 400], default=100)
p.add_argument("--format", choices=["text", "csv"], default="text")
p.add_argument("--pass-mark", type=float, default=45.0)
p.add_argument("-v", "--verbose", action="store_true", help="print extra detail")
p.add_argument("--no-header", dest="header", action="store_false")
print(p.parse_args([]))
print(p.parse_args(["--level", "200", "-v", "--format", "csv", "--pass-mark", "50"]))
print(p.parse_args(["--no-header"]).header)
Output:
Namespace(level=100, format='text', pass_mark=45.0, verbose=False, header=True) Namespace(level=200, format='csv', pass_mark=50.0, verbose=True, header=True) False
typeconverts the text to a number (or applies any function you give it).choicesrestricts the allowed values, so anything else is an error.action="store_true"makes a flag, which is True if present and False otherwise.store_falseworks the opposite way, anddestsets the attribute name.- A short form such as
-vcan be given alongside the long form.
Several Values: nargs
calc = argparse.ArgumentParser(prog="avg")
calc.add_argument("scores", type=float, nargs="+", help="one or more scores")
calc.add_argument("--drop-lowest", action="store_true")
args = calc.parse_args(["72", "58", "90", "45", "--drop-lowest"])
scores = sorted(args.scores)[1:] if args.drop_lowest else args.scores
print(scores, round(sum(scores) / len(scores), 2))
Output:
[58.0, 72.0, 90.0] 73.33
nargs="+" means one or more values, "*" means zero or more, "?" means zero or one, and a number such as 2 means exactly that many. The values arrive as a list.
Mutually Exclusive Options
Some options must not be used together, such as "quiet" and "verbose":
noise = argparse.ArgumentParser(prog="tool")
group = noise.add_mutually_exclusive_group()
group.add_argument("--quiet", action="store_true")
group.add_argument("--verbose", action="store_true")
print(noise.parse_args(["--quiet"]))
err = io.StringIO()
try:
with contextlib.redirect_stderr(err):
noise.parse_args(["--quiet", "--verbose"])
except SystemExit:
print(err.getvalue().strip().splitlines()[-1])
Output:
Namespace(quiet=True, verbose=False) tool: error: argument --verbose: not allowed with argument --quiet
Subcommands
Tools like git have subcommands (git add, git commit), each with its own arguments. argparse supports this with add_subparsers, and set_defaults(func=...) neatly connects each subcommand to the function that handles it.
def cmd_add(args):
print(f"added {args.course} ({args.units} units)")
def cmd_list(args):
print("listing", "all" if args.all else "current", "courses")
app = argparse.ArgumentParser(prog="courses")
sub = app.add_subparsers(dest="command", required=True)
add_p = sub.add_parser("add", help="register a course")
add_p.add_argument("course")
add_p.add_argument("--units", type=int, default=3)
add_p.set_defaults(func=cmd_add)
list_p = sub.add_parser("list", help="show registered courses")
list_p.add_argument("--all", action="store_true")
list_p.set_defaults(func=cmd_list)
for argv in (["add", "MTH101", "--units", "4"], ["list", "--all"]):
args = app.parse_args(argv)
args.func(args)
Output:
added MTH101 (4 units) listing all courses
A Complete Tool: a GPA Calculator
A good structure is to put the logic in functions, build the parser in its own function, and call everything from main(argv=None). Passing None makes argparse read the real command line, while passing a list makes the tool easy to test.
import sys
GRADE_POINTS = {"A": 5, "B": 4, "C": 3, "D": 2, "E": 1, "F": 0}
def build_parser():
p = argparse.ArgumentParser(prog="gpa", description="Calculate a GPA from course results.")
p.add_argument("results", nargs="+", metavar="GRADE:UNITS",
help="results such as A:3 B:2 (grade letter and credit units)")
p.add_argument("--decimals", type=int, default=2, help="decimal places (default: 2)")
return p
def parse_result(text):
grade, _, units = text.partition(":")
if grade.upper() not in GRADE_POINTS or not units.isdigit():
raise ValueError(f"bad result {text!r}: use GRADE:UNITS, e.g. A:3")
return GRADE_POINTS[grade.upper()], int(units)
def main(argv=None):
args = build_parser().parse_args(argv)
try:
pairs = [parse_result(r) for r in args.results]
except ValueError as e:
print("error:", e)
return 1
total_units = sum(u for _, u in pairs)
gpa = sum(p * u for p, u in pairs) / total_units
print(f"GPA: {gpa:.{args.decimals}f} over {total_units} units")
return 0
print(main(["A:3", "B:2", "c:1"]))
print(main(["A:3", "B:3", "--decimals", "1"]))
print(main(["A:3", "X:2"]))
Output:
GPA: 4.33 over 6 units 0 GPA: 4.5 over 6 units 0 error: bad result 'X:2': use GRADE:UNITS, e.g. A:3 1
Saved as gpa.py, the script would finish with these lines, so that its return value becomes the program's exit code (0 for success, anything else for failure):
In a terminal:
if __name__ == "__main__":
sys.exit(main())
# Then, in a terminal:
# python gpa.py A:3 B:2 C:1
# python gpa.py --help
Exit codes let other programs and scripts check whether your tool succeeded.
Common Mistakes with Python argparse
- Forgetting
type=intortype=float, so that the values arrive as strings and arithmetic fails or gives strange results. - Using
type=boolfor a flag.bool("False")is True. Useaction="store_true". - Reading
args.pass-mark. Dashes become underscores:args.pass_mark. - Calling
parse_args()at import time, outsidemain, which makes the module impossible to import or test. - Leaving out help strings, which makes the tool hard for others (and your future self) to use.
Python argparse: Practice Exercises
- Build a parser for
convert, taking a temperature (a float) and a--tooption with choices "C" and "F" (default "F"). Convert the temperature accordingly. - Add a
--roundoption, with a default of 1, for the number of decimal places. - Show that giving
--to Kproduces an error.
Sample solutions:
conv = argparse.ArgumentParser(prog="convert")
conv.add_argument("temperature", type=float)
conv.add_argument("--to", choices=["C", "F"], default="F")
conv.add_argument("--round", type=int, default=1)
def convert(argv):
a = conv.parse_args(argv)
value = a.temperature * 9 / 5 + 32 if a.to == "F" else (a.temperature - 32) * 5 / 9
return round(value, a.round)
print(convert(["37"]), convert(["98.6", "--to", "C"]), convert(["25", "--round", "0"]))
err = io.StringIO()
try:
with contextlib.redirect_stderr(err):
convert(["10", "--to", "K"])
except SystemExit:
print(err.getvalue().strip().splitlines()[-1])
Output:
98.6 37.0 77.0 convert: error: argument --to: invalid choice: 'K' (choose from C, F)
Conclusion
argparse turns a script into a proper command-line tool. Create an ArgumentParser, add positional arguments (required, in order) and optional ones (with dashes, in any order), and read them from the Namespace returned by parse_args. Use type to convert values, default and choices to control them, store_true for flags, nargs for lists, mutually exclusive groups for conflicting options, and subparsers for git-style subcommands. argparse writes the help text and error messages for you, and a main(argv=None) function that returns an exit code keeps the tool easy to test.