$$ \newcommand{\tp}{\thinspace .} $$

This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.

Option-value pairs on the command line

The examples on using command-line arguments so far require the user of the program to type all arguments in their right sequence, just as when calling a function with positional arguments in the right order. It would be very convenient to assign command-line arguments in the same way as we use keyword arguments. That is, arguments are associated with a name, their sequence can be arbitrary, and only the arguments where the default value is not appropriate need to be given. Such type of command-line arguments may have --option value pairs, where option is some name of the argument.

As usual, we shall use an example to illustrate how to work with --option value pairs. Consider the physics formula for the location \( s(t) \) of an object at time \( t \), given that the object started at \( s=s_0 \) at \( t=0 \) with a velocity \( v_0 \), and thereafter was subject to a constant acceleration \( a \): $$ \begin{equation} s(t) = s_0 + v_0t + \frac{1}{2}at^2\tp \tag{1} \end{equation} $$ This formula requires four input variables: \( s_0 \), \( v_0 \), \( a \), and \( t \). We can make a program location.py that takes four options, --s0, --v0, --a, and --t, on the command line. The program is typically run like this:

location.py --t 3 --s0 1 --v0 1 --a 0.5
The sequence of --option value pairs is arbitrary. All options have a default value such that one does not have to specify all options on the command line.

All input variables should have sensible default values such that we can leave out the options for which the default value is suitable. For example, if \( s_0=0 \), \( v_0=0 \), \( a=1 \), and \( t=1 \) by default, and we only want to change \( t \), we can run

location.py --t 3

Basic usage of the argparse module

Python has a flexible and powerful module argparse for reading (parsing) --option value pairs on the command line. Using argparse consists of three steps. First, a parser object must be created:

import argparse
parser = argparse.ArgumentParser()
Second, we need to define the various command-line options,

parser.add_argument('--v0', '--initial_velocity', type=float,
                    default=0.0, help='initial velocity',
                    metavar='v')
parser.add_argument('--s0', '--initial_position', type=float,
                    default=0.0, help='initial position',
                    metavar='s')
parser.add_argument('--a', '--acceleration', type=float,
                    default=1., help='acceleration', metavar='a')
parser.add_argument('--t', '--time', type=float,
                    default=1.0, help='time', metavar='t')
The first arguments to parser.add_argument is the set of options that we want to associate with an input parameter. Optional arguments are the type, a default value, a help string, and a name for the value of the argument (metavar) in a usage string. The argparse module will automatically allow an option -h or --help that prints a usage string for all the registered options. By default, the type is str, the default value is None, the help string is empty, and metavar is the option in upper case without initial dashes.

Third, we must read the command line arguments and interpret them:

args = parser.parse_args()
Through the args object we can extract the values of the various registered parameters: args.v0, args.s0, args.a, and args.t. The name of the parameter is determined by the first option to parser.add_argument, so writing

parser.add_argument('--initial_velocity', '--v0', type=float,
                    default=0.0, help='initial velocity')
will make the initial velocity value appear as args.initial_velocity. We can add the dest keyword to explicitly specify the name where the value is stored:

parser.add_argument('--initial_velocity', '--v0', dest='V0',
                    type=float, default=0.0,
                    help='initial velocity')
Now, args.V0 will retrieve the value of the initial velocity. In case we do not provide any default value, the value will be None.

Our example is completed either by evaluating s as

s = args.s0 + args.v0*t + 0.5*args.a*args.t**2
or by introducing new variables so that the formula aligns better with the mathematical notation:

s0 = args.s0; v0 = args.v0; a = args.a; t = args.t
s = s0 + v0*t + 0.5*a*t**2

A complete program for the example above is found in the file location.py. Try to run it with the -h option to see an automatically generated explanation of legal command-line options.

Mathematical expressions as values

Values on the command line involving mathematical symbols and functions, say --v0 'pi/2', pose a problem with the code example above. The argparse module will in that case try to do float('pi/2') which does not work well since pi is an undefined name. Changing type=float to type=eval is required to interpret the expression pi/2, but even eval('pi/2') fails since pi is not defined inside the argparse module. There are various remedies for this problem.

One can write a tailored function for converting a string value given on the command line to the desired object. For example,

def evalcmlarg(text):
    return eval(text)

parser.add_argument('--s0', '--initial_position', type=evalcmlarg,
                    default=0.0, help='initial position')
The file location_v2.py demonstrates such explicit type conversion through a user-provided conversion function. Note that eval is now taken in the programmer's namespace where (hopefully) pi or other symbols are imported.

More sophisticated conversions are possible. Say \( s_0 \) is specified in terms of a function of some parameter \( p \), like \( s_0=(1-p^2) \). We could then use a string for --s0 and the StringFunction tool from the section Turning string expressions into functions to turn the string into a function:

def toStringFunction4s0(text):
    from scitools.std import StringFunction
    return StringFunction(text, independent_variable='p')

parser.add_argument('--s0', '--initial_position',
                    type=toStringFunction4s0,
                    default='0.0', help='initial position')
Giving a command-line argument --s0 'exp(-1.5) + 10(1-p**2) results in args.s0 being a StringFunction object, which we must evaluate for a p value:

s0 = args.s0
p = 0.05
...
s = s0(p) + v0*t + 0.5*a*t**2
The file location_v3.py contains the complete code for this example.

Another alternative is to perform the correct conversion of values in our own code after the parser object has read the values. To this end, we treat argument types as strings in the parser.add_argument calls, meaning that we replace type=float by set type=str (which is also the default choice of type). Recall that this approach requires specification of default values as strings too, say '0':

parser.add_argument('--s0', '--initial_position', type=str,
                    default='0', help='initial position')
...
from math import *
args.v0 = eval(args.v0)
# or
v0 = eval(args.v0)

s0 = StringFunction(args.s0, independent_variable='p')
p = 0.5
...
s = s0(p) + v0*t + 0.5*a*t**2
Such code is found in the file location_v4.py. You can try out that program with the command-line arguments --s0 'pi/2 + sqrt(p)' --v0 pi/4'.

The final alternative is to write an Action class to handle the conversion from string to the right type. This is the preferred way to perform conversions and well described in the argparse documentation. We shall exemplify it here, but the technicalities involved require understanding of classes (see the document Introduction to classes in Python [1]) and inheritance (see the document Object-oriented programming [2]). For the conversion from string to any object via eval we write

import argparse
from math import *

class ActionEval(argparse.Action):
    def __call__(self, parser, namespace, values,
                 option_string=None):
        setattr(namespace, self.dest, eval(values))
The command-line arguments supposed to be run through eval must then have an action parameter:

parser.add_argument('--v0', '--initial_velocity',
                    default=0.0, help='initial velocity',
                    action=ActionEval)
From string to function via StringFunction for the --s0 argument we write

from scitools.std import StringFunction

class ActionStringFunction4s0(argparse.Action):
    def __call__(self, parser, namespace, values,
                 option_string=None):
        setattr(namespace, self.dest,
                StringFunction(values, independent_variable='p'))
A complete code appears in the file location_v5.py.