Command Line Arguments with Python's Argparse Module

Processing command-line arguments in ad-hoc python tools is one of those areas where I tend to just hack it together from scratch — simply because the effort of learning and understanding the relevant library packages not only seems to be more work than it is worth, but also and in particular more effort than “just doing it” by hand. I don’t want anything fancy, after all. I just want to get it done.

Python’s argparse module is certainly intimidating in this regard: the official Python API reference documentation includes a pointer to an equally official tutorial/howto document — clearly, nobody is expected to understand the API from the reference alone. That does not seem to bode well.

So it came as a considerable surprise to discover that argparse is amazingly convenient. Here is how it works:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument( 'val', help="text" )              # positional arg
parser.add_argument( '-v', '--verbose', help="text" )  # optional arg
args = parser.parse_args()

print( args.val )
print( args.verbose )

That’s it. That’s shorter than any ad-hoc code I could come up with. And that’s not even mentioning any of the under-the-covers functionality that’s provided for free. (See below.)

The argparse module makes a distinction between positional and optional arguments. Positional arguments are generally required and are not preceded by command-line flags. (Think of the file or directory names passed to such Unix commands as mv or cp.) Positional command line arguments will be assigned in the order in which they were defined using add_argument().

Optional arguments are prefixed by one or two dashes. The argparse module considers arguments as positional or optional automatically, based on the presence or absence of that prefix. Somewhat surprisingly, positional and optional arguments can be freely intermingled on the actual command line: it is not necessary for positional arguments to always come last.

Much useful functionality can be accessed through keyword arguments to add_argument(). Here are some of the more useful ones:

  • help : Usage information for the argument.
  • type : Automatic type conversion (e.g. type=int). The value must be a conversion function from string to the desired data type.
  • choices : An array of valid values (e.g. choices=[1,2,3]).
  • default : A default value, if argument is missing (e.g. default=0).
  • required : Unless True, the option is optional.
  • nargs : The number of arguments to consume. The arguments will be returned as a list. Its value is usually an integer, or "?", "*", or "+" to indicate 0 or 1, 0 or all, or at least 1 or all.
  • action : What to do with the argument. The default is "store", meaning that the value is stored in the object returned by parse_args(). Other possibilities include "count", which returns the number of times the option has been given on the command line, and a few others (see below).

In addition to parsing the command-line arguments, the argparse module also performs some input validation and constraint enforcement, as is evident from the choices and required keywords. If input does not meet the expected constraints, then argparse automatically prints a neatly formatted and rather verbose usage message to standard error. In addition to the help keyword, there are additional arguments that can be used to customize the content and format of that message. The option -h (or --help) is provided by default and prints the usage information. (Supply add_help=False to the ArgumentParser() constructor to disable this behavior.)

The two keywords that may not be immediately clear are nargs and action. If nargs is set to an integer, then the corresponding option will consume exactly that many parameters from the command line, and will fail (printing a usage message) if either fewer or more arguments are provided. The symbolic multiplier arguments are available to deal with a variable number of arguments: add_argument( 'files', nargs="+" ) expects at least one, but possibly several arguments, and will store them, as a list, in the files attribute. (The module is fairly smart about edge cases, such as an argument with nargs="*" being followed by another required positional argument and so on. Check the reference documentation or experiment yourself.)

The action keyword indicates what should be done with the received command line argument. By default (action="store"), the value provided on the command line is simply stored as attribute on the object returned by parse_args(). Another possibility is action="count", which stores the number of times the option was used in the command line: useful to set, for example, verbosity levels through repeated use of a -v option.

The two actions "store_true" and "store_false" are used to implement command line switches, that is, command line arguments that don’t take a value, but work simply by being present or absent. (Think of rm -f.) The "store" action expects an argument, and will fail if none is provided. By contrast, "store_true" will store the value True if the option is present, and False otherwise. As an aside, the same effect could be achieved by using the "count" action, and checking whether the count is greater than zero. (Note that the default value for "count" is None, not 0. Use the default keyword to set the value to 0 in case the option is absent.)

That’s it. This should cover all situation in which I would have considered hacking up command line handling from scratch.

Of course there is more; the reference documentation has all the details. With a basic idea of what argparse is trying to achieve, and how it goes about it, the reference page actually makes fairly easy reading.

The main take-away, though, is this. For all the cases that one would consider implementing command line handling from scratch, the following lines of code (properly adapted) will do the trick:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument( '-d', type=int )
parser.add_argument( '-f', '--force', action="store_true" )
parser.add_argument( '-v', '--verbose', action="count", default=0 )
parser.add_argument( 'things', nargs="*", help="help text" )
args = parser.parse_args()

print( args.d, args.force, args.verbose, args.things )

That can’t be beat.

Further reading: