Created
September 17, 2020 18:52
-
-
Save pdarragh/37762ad389fc655d2c1ed300ea1bda8c to your computer and use it in GitHub Desktop.
A brief tutorial of the fundamental functionality of the `argparse` module in the Python standard library.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from shlex import split as split_args # This is used for splitting strings in | |
# the way the shell does it. | |
# The `argparse` module in the standard library provides a ton of functionality | |
# for easily parsing command-line arguments when invoking Python programs. The | |
# full documentation is available here: | |
# | |
# https://docs.python.org/3/library/argparse.html | |
# | |
# In this file, I show some basic examples of how `argparse` works. You can run | |
# the program by doing: | |
# | |
# $ python3 argparse_tutorial.py | |
# | |
# But I will also include the output of any `print` statements in comments | |
# below those statements, where output lines are started with '>'. | |
######################################## | |
## | |
## Simple Positional Arguments | |
## | |
print("") | |
print("Simple Positional Arguments") | |
print("---------------------------") | |
# First, we import the `argparse` module, of course. :) | |
import argparse | |
# Now we create a basic `ArgumentParser` object. This is the starting point | |
# for pretty much any command-line argument parsing with `argparse`. | |
parser = argparse.ArgumentParser() | |
# The most important part of the `ArgumentParser` is the `add_argument` | |
# method, which defines arguments to be parsed from the command line. | |
# I'm going to add a basic *positional argument*. | |
parser.add_argument('my_argument') | |
# The second most important part of the `ArgumentParser` is the ability to | |
# actually parse arguments. This is done with the `parse_args()` method. | |
# | |
# When given no arguments, `parse_args()` will parse the command-line string in | |
# much the same way that using `sys.argv` works. But you can also give it a | |
# list of arguments instead, which is useful in this tutorial because I want to | |
# give my own arguments here instead of relying on calling the program from the | |
# command line. | |
command_line_args = split_args("argument") # The `split_args()` function came | |
# from the import on line 3! | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(my_argument='argument') | |
# The `parse_args()` method returns a `Namespace` object. This is simply an | |
# object that has fields with the same names as the arguments you specified, | |
# and the values of those fields are the values parsed from the command line. | |
# | |
# So, for instance, we can directly access the `my_argument` argument: | |
print("The value of my_argument is: " + args.my_argument) | |
# > The value of my_argument is: argument | |
# In this example, `my_argument` is called a *positional argument*, which means | |
# that `argparse` determines its value purely based on its position in the | |
# arguments list. Positional arguments are, by default, not optional. Every | |
# positional argument *must* be provided when calling the program! | |
# | |
# Let's put a couple positional arguments together. | |
parser = argparse.ArgumentParser() | |
parser.add_argument('foo') | |
parser.add_argument('bar') | |
# First, we'll properly give both arguments. | |
command_line_args = split_args("foo-arg and-the-bar-arg") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(bar='and-the-bar-arg', foo='foo-arg') | |
# If we leave out either of the arguments, we'll get an error! | |
# | |
# NOTE: The `ArgumentParser` will also print usage information, shown below. | |
try: | |
command_line_args = split_args("just-the-foo-arg") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
except SystemExit: | |
print("Got an error!") | |
# > usage: argparse_tutorial.py [-h] foo bar | |
# > argparse_tutorial.py: error: the following arguments are required: bar | |
# > Got an error! | |
# | |
# So from this we see that `argparse` was upset we didn't supply the `bar` | |
# argument. Pretty useful! | |
######################################## | |
## | |
## Simple Optional Arguments | |
## | |
print("") | |
print("Simple Optional Arguments") | |
print("-------------------------") | |
# An *optional argument* is an argument that is, well, optional! These are | |
# created when you name the argument with a leading dash. | |
# | |
# Let's create a new parser to show some optional arguments! | |
parser = argparse.ArgumentParser() | |
# We'll start with just a single optional argument. | |
parser.add_argument('--my_optional') | |
# Now, like before, we'll create an invocation of the program's arguments. | |
command_line_args = split_args("--my_optional 'this is the optional value'") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(my_optional='this is the optional value') | |
# As before, we can access this value directly: | |
print("The value of my_optional is: " + args.my_optional) | |
# > The value of my_optional is: this is the optional value | |
# But, as you can imagine, optional values are *optional* --- which means we | |
# can also just not give it in the arguments. | |
command_line_args = split_args("") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(my_optional=None) | |
# So when an optional argument isn't given in the arguments list, its value in | |
# the `Namespace` object created by `parse_args()` is `None`. | |
if args.my_optional is None: | |
print("Yes, my_optional is None.") | |
else: | |
print("No, there was a value in my_optional!") | |
# > Yes, my_optional is None. | |
# Optional arguments allow us to give users the ability to specify additional | |
# constraints when needed. | |
# | |
# They also have a nifty syntax for shorthand names! | |
parser = argparse.ArgumentParser() | |
parser.add_argument('-f', '--foo') | |
parser.add_argument('-b', '--bar') | |
command_line_args = split_args("--foo 'this is the value for foo' -b 'and this is for bar'") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(bar='and this is for bar', foo='this is the value for foo') | |
# And we can just give one or the other (or neither) as we desire! | |
command_line_args = split_args("-b 'just the bar this time'") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(bar='just the bar this time') | |
######################################## | |
## | |
## Actions | |
## | |
print("") | |
print("Actions") | |
print("-------") | |
# Arguments can also perform *actions*. Most of the time, you won't need this, | |
# but one of the supplied actions is very useful: `store_true`. This is | |
# generally used on an optional argument so that you just find out whether the | |
# argument was supplied instead of taking an additional bit of text. | |
parser = argparse.ArgumentParser() | |
parser.add_argument('--optional-boolean', action='store_true') | |
command_line_args = split_args("--optional-boolean") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(optional_boolean=True) | |
# If the argument is not supplied, the value is False instead of None. | |
command_line_args = split_args("") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(optional_boolean=False) | |
# Another handy action is the `append` action, which allows you to specify an | |
# argument multiple times and `argparse` builds a list of those arguments. | |
parser = argparse.ArgumentParser() | |
parser.add_argument('--name', action='append') | |
command_line_args = split_args("--name Pierce --name Kaelin --name Ina") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(name=['Pierce', 'Kaelin', 'Ina']) | |
# With the `append` action, if you don't specify the argument at all then you | |
# still end up with a `None`: | |
command_line_args = split_args("") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(name=None) | |
# NOTE: The `append` action shouldn't be used with positional arguments because | |
# it doesn't work how you'd think. Positional arguments can only be given | |
# one time (by default), so if you try to give them multiple times to | |
# build a list you'll get an error. There are other ways, which I'll | |
# explain later! | |
######################################## | |
## | |
## Types | |
## | |
print("") | |
print("Types") | |
print("-----") | |
# You can also specify the *type* of an argument. Really, this is a function | |
# that takes a string and produces a value, which will then be stored in the | |
# `Namespace` object for you to use. This is a great way to parse the arguments | |
# given on the command line! | |
# | |
# A simple example of this is to specify an argument that takes only integers. | |
parser = argparse.ArgumentParser() | |
parser.add_argument('some_int', type=int) # Remember that `int` is a function! | |
command_line_args = split_args("42") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(some_int=42) | |
# NOTE: The value is `42` and not `'42'` --- it's an integer, not a string! | |
# Another great way to use this is for handling file names. The `pathlib` | |
# module in the standard library provides a `Path` class that can be used as an | |
# argument type in the `ArgumentParser`: | |
from pathlib import Path | |
parser = argparse.ArgumentParser() | |
parser.add_argument('filename', type=Path) | |
command_line_args = split_args("/path/to/a/file") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(filename=PosixPath('/path/to/a/file')) | |
# NOTE: This function by itself does not validate whether a path exists; it | |
# only converts an arbitrary string into a `Path` object that you can | |
# then use to do validation. For example, we could now check if the | |
# given path actually exists! | |
if args.filename.is_file(): | |
print("Yes, it's a file!") | |
else: | |
print("No, not a file.") | |
# > No, not a file. | |
######################################## | |
## | |
## Choices | |
## | |
print("") | |
print("Choices") | |
print("-------") | |
# Although specifying the type of an argument can be useful, that may still be | |
# too broad to be a useful restriction in some cases. Sometimes you only want | |
# your users to be able to choose from a handful of options! For that, | |
# `argparse` provides *choices*. | |
parser = argparse.ArgumentParser() | |
parser.add_argument('pet', choices=['cat', 'dog', 'fish']) | |
command_line_args = split_args("cat") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(pet='cat') | |
# If you try to give the argument a value not given in the list of choices, | |
# you'll get an error. | |
try: | |
command_line_args = split_args("emu") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
except SystemExit: | |
print("Yep, got an error!") | |
# > usage: argparse_tutorial.py [-h] {cat,dog,fish} | |
# > argparse_tutorial.py: error: argument pet: invalid choice: 'emu' (choose from 'cat', 'dog', 'fish') | |
# > Yep, got an error! | |
######################################## | |
## | |
## Default Values | |
## | |
print("") | |
print("Default Values") | |
print("--------------") | |
# We can supply default values to make things easier on our users. | |
# NOTE: You should really only use this with optional arguments! | |
parser = argparse.ArgumentParser() | |
parser.add_argument('--name', default='Steve Rogers') | |
command_line_args = split_args("") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(name='Steve Rogers') | |
######################################## | |
## | |
## Number of Arguments | |
## | |
print("") | |
print("Number of Arguments") | |
print("-------------------") | |
# By default, `argparse` arguments only take one value. But we can change that! | |
# We use `nargs` to specify the number of values an argument must take. If the | |
# number is greater than 1, the argument's values will be put into a list. | |
parser = argparse.ArgumentParser() | |
parser.add_argument('foo', nargs=2) | |
command_line_args = split_args("foo1 foo2") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(foo=['foo1', 'foo2']) | |
# NOTE: If you use the wrong number of arguments, you'll get an error! | |
# There are a lot of neat configurations of `nargs`. For example, we can turn | |
# a positional argument (usually required) into an optional argument by doing: | |
parser = argparse.ArgumentParser() | |
parser.add_argument('foo', nargs='?') | |
command_line_args = split_args("") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(foo=None) | |
# NOTE: You should probably not do this. In most cases, if you want an optional | |
# positional argument, you should just use an actual optional argument | |
# with the '--foo' kind of names! | |
# We can also say that an argument can be given any number of times: | |
parser = argparse.ArgumentParser() | |
parser.add_argument('--names', nargs='*') | |
command_line_args = split_args("--names Pierce Kaelin") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(names=['Pierce', 'Kaelin']) | |
command_line_args = split_args("--names") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(names=[]) | |
command_line_args = split_args("") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(names=None) | |
# NOTE: There is a difference between not giving an argument (this example) and | |
# giving an argument with zero values (previous example). | |
# We can also tell `argparse` to "accept one or more values for this argument", | |
# like so: | |
parser = argparse.ArgumentParser() | |
parser.add_argument('words', nargs='+') | |
command_line_args = split_args("alpha bravo charlie") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(words=['alpha', 'bravo', 'charlie']) | |
######################################## | |
## | |
## Help text | |
## | |
print("") | |
print("Help Text") | |
print("---------") | |
# With `argparse`, you can easily provide useful help text to your users! | |
# | |
# First, we can add a description of the program to the `ArgumentParser`: | |
parser = argparse.ArgumentParser(description="An example argument parser.") | |
# Then, for each argument added we can provide argument-specific help text. | |
parser.add_argument('input_file', help='the file to use for input') | |
# This information can be seen when we give the `-h` or `--help` options. | |
# NOTE: Unfortunately for this tutorial (but fortunately for real use cases), | |
# `argparse` actually looks for this option before anything else and will | |
# exit immediately after printing the help text. So to keep going here, | |
# we'll have to prevent the exit! | |
try: | |
command_line_args = split_args("--help") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
except SystemExit: | |
pass | |
# > usage: argparse_tutorial.py [-h] input_file | |
# > | |
# > An example argument parser. | |
# > | |
# > positional arguments: | |
# > input_file the file to use for input | |
# > | |
# > optional arguments: | |
# > -h, --help show this help message and exit | |
# NOTE: The name of the program (`argparse_tutorial.py` in this case) is | |
# determined by `argparse` automatically. | |
######################################## | |
## | |
## Combining Functionality | |
## | |
print("") | |
print("Combining Functionality") | |
print("-----------------------") | |
# The real power of `argparse` comes when you combine these features together. | |
# Let's start with a basic command line parser that takes in a file name as an | |
# argument. | |
parser = argparse.ArgumentParser(description="The final tutorial example.") | |
parser.add_argument('input_file', type=Path, | |
help="an input file for input") | |
# Now we'll add an output file as an optional argument, but we also want to | |
# include a default name for that file. | |
DEFAULT_OUTPUT_FILE = Path('output.txt') | |
parser.add_argument('--output-file', type=Path, default=DEFAULT_OUTPUT_FILE, | |
help="an output file for output") | |
# Now we want our user to tell us some kinds of pets they want to run the | |
# program with. We'll let them specify this option a few times, but they must | |
# give it at least once and we'll only let them give certain pets as values! | |
PETS = ['cat', 'dog', 'fish'] | |
parser.add_argument('--pet', choices=PETS, nargs='+', | |
help="pets; can give any number as long as it's more than one") | |
# We'll also let them tell us whether it's a weekend or not. | |
parser.add_argument('--is-weekend', action='store_true', | |
help="whether it is the weekend; defaults to False") | |
# Now let's try it out! | |
command_line_args = split_args("/path/to/input-file --pet cat cat dog") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
# > Namespace(input_file=PosixPath('/path/to/input-file'), is_weekend=False, output_file=PosixPath('output.txt'), pet=['cat', 'cat', 'dog']) | |
# Let's also check out the help text: | |
try: | |
command_line_args = split_args("-h") | |
args = parser.parse_args(command_line_args) | |
print(args) | |
except SystemExit: | |
pass | |
# > usage: argparse_tutorial.py [-h] [--output-file OUTPUT_FILE] [--pet {cat,dog,fish} [{cat,dog,fish} ...]] | |
# > [--is-weekend] | |
# > input_file | |
# > | |
# > The final tutorial example. | |
# > | |
# > positional arguments: | |
# > input_file an input file for input | |
# > | |
# > optional arguments: | |
# > -h, --help show this help message and exit | |
# > --output-file OUTPUT_FILE | |
# > an output file for output | |
# > --pet {cat,dog,fish} [{cat,dog,fish} ...] | |
# > pets; can give any number as long as it's more than one | |
# > --is-weekend whether it is the weekend; defaults to False | |
################################################################################ | |
## | |
## Conclusion | |
## | |
## The `argparse` module is hugely powerful. Pretty much any time you want to | |
## do some handling of command-line arguments, you should use `argparse`. I | |
## never use `sys.argv` anymore, because `argparse` is so simple and produces | |
## much better results. | |
## | |
## There is a lot more functionality given in `argparse`, such as subcommands, | |
## but I'm leaving that out here so I can keep this tutorial brief. I hope it's | |
## been helpful! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment