Skip to content

Instantly share code, notes, and snippets.

@pdarragh
Created September 17, 2020 18:52
Show Gist options
  • Save pdarragh/37762ad389fc655d2c1ed300ea1bda8c to your computer and use it in GitHub Desktop.
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.
#!/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