Tutoriel Argparse

author:Tshepang Lekhonkhobe

Ce tutoriel est destiné à être une introduction en douceur à argparse, le module d’analyse de ligne de commande recommandé dans la bibliothèque standard de Python.

Note

Il y a deux autres modules qui remplissent le même rôle : getopt (un équivalent de getopt() du langage C) et mod:optparse qui est obsolète. Il faut noter que argparse est basé sur optparse et donc s’utilise de manière très similaire.

Concepts

Commençons par l’utilisation de la commande ls pour voir le type de fonctionnalité que nous allons étudier dans ce tutoriel d’introduction :

$ ls
cpython  devguide  prog.py  pypy  rm-unused-function.patch
$ ls pypy
ctypes_configure  demo  dotviewer  include  lib_pypy  lib-python ...
$ ls -l
total 20
drwxr-xr-x 19 wena wena 4096 Feb 18 18:51 cpython
drwxr-xr-x  4 wena wena 4096 Feb  8 12:04 devguide
-rwxr-xr-x  1 wena wena  535 Feb 19 00:05 prog.py
drwxr-xr-x 14 wena wena 4096 Feb  7 00:59 pypy
-rw-r--r--  1 wena wena  741 Feb 18 01:01 rm-unused-function.patch
$ ls --help
Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
...

Quelques concepts que l’on peut apprendre avec les quatre commandes :

  • La commande ls est utile quand elle est exécutée sans aucune option. Par défaut cela affiche le contenu du dossier courant.
  • Si l’on veut plus que ce qui est proposé par défaut, il faut l’indiquer. Dans le cas présent, on veut afficher un dossier différent : pypy. Ce que l’on a fait c’est spécifier un argument positionnel. C’est appelé ainsi car cela permet au programme de savoir quoi faire avec la valeur seulement en se basant sur sa position dans la ligne de commande. Ce concept est plus pertinent pour une commande comme cp dont l’usage de base est cp SRC DEST. Le premier argument est ce que vous voulez copier et le second est où vous voulez le copier.
  • Maintenant, supposons que l’on veut changer la façon dont le programme agit. Dans notre exemple, on affiche plus d’information pour chaque ficher que simplement leur nom. Dans ce cas, -l est un argument optionnel.
  • C’est un fragment du texte d’aide. Cela peut être très utile quand on tombe sur un programme que l’on à jamais utilisé auparavant car on peut comprendre sont fonctionnement simplement en lisant l’aide associée.

Les bases

Commençons par un exemple très simple qui ne fait (quasiment) rien :

import argparse
parser = argparse.ArgumentParser()
parser.parse_args()

Ce qui suit est le résultat de l’exécution du code :

$ python3 prog.py
$ python3 prog.py --help
usage: prog.py [-h]

optional arguments:
  -h, --help  show this help message and exit
$ python3 prog.py --verbose
usage: prog.py [-h]
prog.py: error: unrecognized arguments: --verbose
$ python3 prog.py foo
usage: prog.py [-h]
prog.py: error: unrecognized arguments: foo

Voilà ce qu’il ce passe :

  • Exécuter le script sans aucune option à pour effet que rien est affiché sur stdout. Ce n’est pas très utile.
  • Le deuxième commence à montrer l’intérêt du module argparse. On a quasiment rien fait mais on a déjà un beau message d’aide.
  • L’option --help, que l’on peut aussi raccourcir en -h, est la seule option que l’on a gratuitement (i.e. pas besoin de la préciser). Préciser quoi que ce soit d’autre entrainera une erreur. Mais même dans ce cas, on reçoit aussi un message utile, toujours gratuitement.

Introduction aux arguments positionnels

Un exemple :

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo")
args = parser.parse_args()
print(args.echo)

On exécute le code :

$ python3 prog.py
usage: prog.py [-h] echo
prog.py: error: the following arguments are required: echo
$ python3 prog.py --help
usage: prog.py [-h] echo

positional arguments:
  echo

optional arguments:
  -h, --help  show this help message and exit
$ python3 prog.py foo
foo

Voilà ce qu’il ce passe :

  • On a ajouté la méthode add_argument() que l’on utilise pour préciser quelles options de lignes de commandes le programme peut accepter. Dans le cas présent, je l’ai appelé echo pour que cela corresponde à sa fonction.
  • Utiliser le programme nécessite maintenant que l’on précise une option.
  • La méthode parse_args() renvoie en réalité certaines données des options précisées, dans le cas présent : echo.
  • La variable est comme une forme de “magie” que argparse effectue gratuitement (i.e. pas besoin de préciser dans quelle variable la valeur est stockée). Vous aurez aussi remarqué que le nom est le même que l’argument en chaîne de caractère donné à la méthode : echo.

Notez cependant que, même si l’affichage d’aide paraît bien , il n’est pas aussi utile qu’il pourrait l’être. Par exemple, on peut lire que echo est un argument positionnel mais on ne peut pas savoir ce que cela fait autrement qu’en le devinant ou en lisant le code source. Donc, rendons le un peu plus utile :

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo", help="echo the string you use here")
args = parser.parse_args()
print(args.echo)

Et on obtient :

$ python3 prog.py -h
usage: prog.py [-h] echo

positional arguments:
  echo        echo the string you use here

optional arguments:
  -h, --help  show this help message and exit

Maintenant, qu’en dîtes vous s’il on fait quelque chose d’encore plus utile :

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", help="display a square of a given number")
args = parser.parse_args()
print(args.square**2)

Ce qui suit est le résultat de l’exécution du code :

$ python3 prog.py 4
Traceback (most recent call last):
  File "prog.py", line 5, in <module>
    print(args.square**2)
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

Cela n’a pas très bien fonctionné. C’est parce que argparse traite les options que l’on donnes comme des chaînes de caractères à moins qu’on ne lui indique de faire autrement. Donc, disons à argparse de traiter cette entrée comme un entier :

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", help="display a square of a given number",
                    type=int)
args = parser.parse_args()
print(args.square**2)

Ce qui suit est le résultat de l’exécution du code :

$ python3 prog.py 4
16
$ python3 prog.py four
usage: prog.py [-h] square
prog.py: error: argument square: invalid int value: 'four'

Cela a bien fonctionné. Maintenant le programme va même s’arrêter si l’entrée n’est pas légale avant de procéder à l’exécution.

Introduction aux arguments optionnels

Jusqu’à maintenant, on a joué avec les arguments positionnels. Regardons comment en ajouter des optionnels :

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbosity", help="increase output verbosity")
args = parser.parse_args()
if args.verbosity:
    print("verbosity turned on")

Et le résultat :

$ python3 prog.py --verbosity 1
verbosity turned on
$ python3 prog.py
$ python3 prog.py --help
usage: prog.py [-h] [--verbosity VERBOSITY]

optional arguments:
  -h, --help            show this help message and exit
  --verbosity VERBOSITY
                        increase output verbosity
$ python3 prog.py --verbosity
usage: prog.py [-h] [--verbosity VERBOSITY]
prog.py: error: argument --verbosity: expected one argument

Voilà ce qu’il ce passe :

  • Le programme est écrit de sorte qu’il n’affiche rien sauf si l’option --verbosity est présicée.
  • Pour montrer que l’option est bien optionnelle il n’y aura pas d’erreur s’il on exécute le programme sans celle-ci. Notez que par défaut, si une option n’est pas utilisée, la variable associée, dans le cas présent : args.verbosity, prend la valeur None c’est pourquoi elle échoue le test de vérité de l’assertion if.
  • Le message d’aide est quelque peu différent.
  • Quand on utilise l’option --verbosity on doit aussi préciser une valeur, n’importe laquelle.

L’exemple ci-dessus accepte des valeurs entières arbitraires pour --verbosity mais pour notre programme simple seule deux valeurs sont réellement utiles : True et False. Modifions le code en accord avec cela :

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", help="increase output verbosity",
                    action="store_true")
args = parser.parse_args()
if args.verbose:
    print("verbosity turned on")

Et le résultat :

$ python3 prog.py --verbose
verbosity turned on
$ python3 prog.py --verbose 1
usage: prog.py [-h] [--verbose]
prog.py: error: unrecognized arguments: 1
$ python3 prog.py --help
usage: prog.py [-h] [--verbose]

optional arguments:
  -h, --help  show this help message and exit
  --verbose   increase output verbosity

Voilà ce qu’il ce passe :

  • Maintenant l’option est plus un flag (ou drapeau) que quelque chose qui nécessite une valeur. On a même changé le nom de l’option pour qu’elle corresponde à cette idée. Notez que maintenant on précise une nouvelle action clavier et qu’on lui donne la valeur "store_true". Cela signifie que si l’option est précisée la valeur True est assignée à args.verbose. Ne rien préciser implique la valeur False.
  • Dans l’esprit de ce que sont vraiment les flags (ou drapeaux), il se plaint quand vous tentez de préciser une valeur.
  • Notez que l’aide est différente.

Les options raccourcies

Si vous êtes familier avec l’utilisation des ligne de commande vous avez dû remarqué que je n’ai pour l’instant rien dit au sujet des versions raccourcies des options. C’est très simple :

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", help="increase output verbosity",
                    action="store_true")
args = parser.parse_args()
if args.verbose:
    print("verbosity turned on")

Et voilà :

$ python3 prog.py -v
verbosity turned on
$ python3 prog.py --help
usage: prog.py [-h] [-v]

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose  increase output verbosity

Notez que la nouvelle option est aussi indiquée dans l’aide.

Combinaison d’arguments positionnels et optionnels

Notre programme continue de grandir en complexité :

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbose", action="store_true",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbose:
    print("the square of {} equals {}".format(args.square, answer))
else:
    print(answer)

Et voilà le résultat :

$ python3 prog.py
usage: prog.py [-h] [-v] square
prog.py: error: the following arguments are required: square
$ python3 prog.py 4
16
$ python3 prog.py 4 --verbose
the square of 4 equals 16
$ python3 prog.py --verbose 4
the square of 4 equals 16
  • We’ve brought back a positional argument, hence the complaint.
  • Note that the order does not matter.

How about we give this program of ours back the ability to have multiple verbosity values, and actually get to use them:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", type=int,
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print("the square of {} equals {}".format(args.square, answer))
elif args.verbosity == 1:
    print("{}^2 == {}".format(args.square, answer))
else:
    print(answer)

Et le résultat :

$ python3 prog.py 4
16
$ python3 prog.py 4 -v
usage: prog.py [-h] [-v VERBOSITY] square
prog.py: error: argument -v/--verbosity: expected one argument
$ python3 prog.py 4 -v 1
4^2 == 16
$ python3 prog.py 4 -v 2
the square of 4 equals 16
$ python3 prog.py 4 -v 3
16

These all look good except the last one, which exposes a bug in our program. Let’s fix it by restricting the values the --verbosity option can accept:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2],
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print("the square of {} equals {}".format(args.square, answer))
elif args.verbosity == 1:
    print("{}^2 == {}".format(args.square, answer))
else:
    print(answer)

Et le résultat :

$ python3 prog.py 4 -v 3
usage: prog.py [-h] [-v {0,1,2}] square
prog.py: error: argument -v/--verbosity: invalid choice: 3 (choose from 0, 1, 2)
$ python3 prog.py 4 -h
usage: prog.py [-h] [-v {0,1,2}] square

positional arguments:
  square                display a square of a given number

optional arguments:
  -h, --help            show this help message and exit
  -v {0,1,2}, --verbosity {0,1,2}
                        increase output verbosity

Note that the change also reflects both in the error message as well as the help string.

Now, let’s use a different approach of playing with verbosity, which is pretty common. It also matches the way the CPython executable handles its own verbosity argument (check the output of python --help):

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display the square of a given number")
parser.add_argument("-v", "--verbosity", action="count",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print("the square of {} equals {}".format(args.square, answer))
elif args.verbosity == 1:
    print("{}^2 == {}".format(args.square, answer))
else:
    print(answer)

We have introduced another action, « count », to count the number of occurrences of a specific optional arguments:

$ python3 prog.py 4
16
$ python3 prog.py 4 -v
4^2 == 16
$ python3 prog.py 4 -vv
the square of 4 equals 16
$ python3 prog.py 4 --verbosity --verbosity
the square of 4 equals 16
$ python3 prog.py 4 -v 1
usage: prog.py [-h] [-v] square
prog.py: error: unrecognized arguments: 1
$ python3 prog.py 4 -h
usage: prog.py [-h] [-v] square

positional arguments:
  square           display a square of a given number

optional arguments:
  -h, --help       show this help message and exit
  -v, --verbosity  increase output verbosity
$ python3 prog.py 4 -vvv
16
  • Yes, it’s now more of a flag (similar to action="store_true") in the previous version of our script. That should explain the complaint.
  • It also behaves similar to « store_true » action.
  • Now here’s a demonstration of what the « count » action gives. You’ve probably seen this sort of usage before.
  • And, just like the « store_true » action, if you don’t specify the -v flag, that flag is considered to have None value.
  • As should be expected, specifying the long form of the flag, we should get the same output.
  • Sadly, our help output isn’t very informative on the new ability our script has acquired, but that can always be fixed by improving the documentation for out script (e.g. via the help keyword argument).
  • That last output exposes a bug in our program.

Let’s fix:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", action="count",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2

# bugfix: replace == with >=
if args.verbosity >= 2:
    print("the square of {} equals {}".format(args.square, answer))
elif args.verbosity >= 1:
    print("{}^2 == {}".format(args.square, answer))
else:
    print(answer)

And this is what it gives:

$ python3 prog.py 4 -vvv
the square of 4 equals 16
$ python3 prog.py 4 -vvvv
the square of 4 equals 16
$ python3 prog.py 4
Traceback (most recent call last):
  File "prog.py", line 11, in <module>
    if args.verbosity >= 2:
TypeError: unorderable types: NoneType() >= int()
  • First output went well, and fixes the bug we had before. That is, we want any value >= 2 to be as verbose as possible.
  • Third output not so good.

Let’s fix that bug:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", action="count", default=0,
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity >= 2:
    print("the square of {} equals {}".format(args.square, answer))
elif args.verbosity >= 1:
    print("{}^2 == {}".format(args.square, answer))
else:
    print(answer)

We’ve just introduced yet another keyword, default. We’ve set it to 0 in order to make it comparable to the other int values. Remember that by default, if an optional argument isn’t specified, it gets the None value, and that cannot be compared to an int value (hence the TypeError exception).

And:

$ python3 prog.py 4
16

You can go quite far just with what we’ve learned so far, and we have only scratched the surface. The argparse module is very powerful, and we’ll explore a bit more of it before we end this tutorial.

Getting a little more advanced

What if we wanted to expand our tiny program to perform other powers, not just squares:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
parser.add_argument("-v", "--verbosity", action="count", default=0)
args = parser.parse_args()
answer = args.x**args.y
if args.verbosity >= 2:
    print("{} to the power {} equals {}".format(args.x, args.y, answer))
elif args.verbosity >= 1:
    print("{}^{} == {}".format(args.x, args.y, answer))
else:
    print(answer)

Sortie :

$ python3 prog.py
usage: prog.py [-h] [-v] x y
prog.py: error: the following arguments are required: x, y
$ python3 prog.py -h
usage: prog.py [-h] [-v] x y

positional arguments:
  x                the base
  y                the exponent

optional arguments:
  -h, --help       show this help message and exit
  -v, --verbosity
$ python3 prog.py 4 2 -v
4^2 == 16

Notice that so far we’ve been using verbosity level to change the text that gets displayed. The following example instead uses verbosity level to display more text instead:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
parser.add_argument("-v", "--verbosity", action="count", default=0)
args = parser.parse_args()
answer = args.x**args.y
if args.verbosity >= 2:
    print("Running '{}'".format(__file__))
if args.verbosity >= 1:
    print("{}^{} == ".format(args.x, args.y), end="")
print(answer)

Sortie :

$ python3 prog.py 4 2
16
$ python3 prog.py 4 2 -v
4^2 == 16
$ python3 prog.py 4 2 -vv
Running 'prog.py'
4^2 == 16

Conflicting options

So far, we have been working with two methods of an argparse.ArgumentParser instance. Let’s introduce a third one, add_mutually_exclusive_group(). It allows for us to specify options that conflict with each other. Let’s also change the rest of the program so that the new functionality makes more sense: we’ll introduce the --quiet option, which will be the opposite of the --verbose one:

import argparse

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(answer)
elif args.verbose:
    print("{} to the power {} equals {}".format(args.x, args.y, answer))
else:
    print("{}^{} == {}".format(args.x, args.y, answer))

Our program is now simpler, and we’ve lost some functionality for the sake of demonstration. Anyways, here’s the output:

$ python3 prog.py 4 2
4^2 == 16
$ python3 prog.py 4 2 -q
16
$ python3 prog.py 4 2 -v
4 to the power 2 equals 16
$ python3 prog.py 4 2 -vq
usage: prog.py [-h] [-v | -q] x y
prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
$ python3 prog.py 4 2 -v --quiet
usage: prog.py [-h] [-v | -q] x y
prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose

That should be easy to follow. I’ve added that last output so you can see the sort of flexibility you get, i.e. mixing long form options with short form ones.

Before we conclude, you probably want to tell your users the main purpose of your program, just in case they don’t know:

import argparse

parser = argparse.ArgumentParser(description="calculate X to the power of Y")
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(answer)
elif args.verbose:
    print("{} to the power {} equals {}".format(args.x, args.y, answer))
else:
    print("{}^{} == {}".format(args.x, args.y, answer))

Note that slight difference in the usage text. Note the [-v | -q], which tells us that we can either use -v or -q, but not both at the same time:

$ python3 prog.py --help
usage: prog.py [-h] [-v | -q] x y

calculate X to the power of Y

positional arguments:
  x              the base
  y              the exponent

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose
  -q, --quiet

Conclusion

The argparse module offers a lot more than shown here. Its docs are quite detailed and thorough, and full of examples. Having gone through this tutorial, you should easily digest them without feeling overwhelmed.