Using Argparse in Python – a quick reference guide
This post is inspired by an example in this video by John Hammond (no, not the Weatherman – the information security researcher and communicator). The main content of that video is actually irrelevant to this post; but during the video John makes use of the Python library Argparse to add command-line arguments to the tool he is writing.
Without criticising John at all here, he does a good job of showing that the documentation is perhaps less than ideal for someone who just wants to use the tool… I’ve used Argparse quite a bit in various bits of code I’ve written over the few years, so I thought that I’d have a stab at illustrating how to use argparse in the context of the use-case shown in the video. Hopefully along the way I’ll also show how you can use it for some other things too – but this isn’t an exhaustive guide and I won’t cover everything that argparse can do.
Our goal here is to illustrate a simple example of how to use argparse to handle a slightly non-trivial (but quite common) case. What we want to do, is to write something to enable our tool (I won’t show the tool itself – but it could be either the one seen in the video, or anything else that might require similar options) to run in one of two modes: ‘local’ or ‘remote’; and when we’re running it in the remote mode – we are required to also specify the hostname (target) which we’re going to connect to, and the port number we should use for that connection. If we’re running in the local mode however, we don’t require (and shouldn’t specify) these arguments.
We’ll start by thinking about how to specify the mode we’re running in. One simple way to do this; to use a positional argument, (that is one, where the position in the sequence we enter on the command-line) determines what it is.
import argparse # Instantiate our parser – and supply a description for the help mode parser = argparse.ArgumentParser(description="Python Argparse demo") # Add a simple positional argument, but constrain the values to # one of the two choices we supply... parser.add_argument('mode', type=str, choices=['local', 'remote']) # Actually do the parsing... args = parser.parse_args() # Other setup code might go here, if we had some... # ... # Now we can select what to do on the basis of the selection option if args.mode == 'remote': do_remote_stuff() else: do_local_stuff()
Whilst this would work (for the first part of our requirement – we’ll come on to the second half later); I’m personally not a huge fan of positional arguments for anything other than a single argument being supplied for an input filename… Perhaps a tool like gcc is a good example of that..
e.g. we might run:
$ demo file.dat
So instead, let’s look at another way that we could do this; by using a single required option argument to select the mode. This feels like a better fit (to me) for a situation where we have simple fixed-choice argument like a mode…
In this example we’ll also add some additional help text; to give us a nicer output when we run our tool with the --help
mode enabled.
import argparse # Instantiate our parser – and supply a description for the help mode parser = argparse.ArgumentParser(description="Python Argparse demo") parser.add_argument('--mode', '-m', required=True, type=str, choices=['local', 'remote'], help="Specify a local or a remote target to run against.") # Actually do the parsing... args = parser.parse_args() # Other setup code might go here, if we had some... # ... # Now we can select what to do on the basis of the selection option if args.mode == 'remote': do_remote_stuff() else: do_local_stuff()
But that was the easy part. What about the second half of the requirements? How do we require that the user specifies a target and a port – if (and only if) our mode was set to remote?
If we didn’t care about when these were used we could add them in the usual way.
parser.add_argument('--host', '-t', type=str, help="Specify an address for the tool to run against, when using remote mode.") parser.add_argument('--port', '-p', type=int, help="Specify a port to connect to the host on, when using remote mode.")
By the way, note here that we don’t need to set defaults – as the argument will default to a Python None
if nothing is specified; and we can easily can easily check for this.
Given that we do want to constrain (and also conditionally mandate) the use of these arguments, we have a few different options on how to do this.
A commonly seen solution to this is to dynamically specify the required
parameter for the argument, based on the value of sys.argv
.
e.g.
parser.add_argument('--port', required=('remote' in sys.argv))
But this feels rather like we’re trying to write C, in Python…
A better solution is to turn the whole thing around and to have separate “local” and “remote” arguments. Note that if we do this we need to set them to be mutually exclusive (since we can’t be in both local, and remote modes at the same time). To do this we can simply implement an argparser
group… We can specify that one of the elements within this group is required, by making the group itself ‘required’.
mutex_group = parser.add_mutually_exclusive_group(required=True)
We could do this by using arguments that use the action store_true
, to set a boolean flag if they’re present.
mutex_group.add_argument("--local", '-l', action='store_true', help="Specify the tool to run in local mode.") mutex_group.add_argument("--remote", '-r', action='store_true', help="Specify the tool to run in remote mode. Target & Port must be set.")
We could change the remote argument, to use positional elements, for the target & port; by setting nargs
and a metavar
to name them.
mutex_group.add_argument("--remote", nargs=2, help="Specify the tool to run in remote mode. Both a target address and port number must be specified", metavar=('TARGET', 'PORT'))
This is arguably better than the sys.argv
solution (IMHO); and it needs less tweaking to make the help-text read usefully.
It is possible to implement something a bit like this using subparsers
(the cannonical use-case for which is a git-like tool, with multiple sub-commands, each of which have some arguments). However, it’s quite fragile to being used in a particular way and I’ve not found a way to implement this specific requirement using subparsers
.
Moreover even if there was a way, we’re inherrently back to the use of possitional arguments…
The final way is to think about this in a purely UNIX way. We’re kind of already there with all of the above; but in terms of requiring the minimum of information, we can see that we can turn this around again… The port and target are only required (and meaningful) when we’re in remote mode; so if we don’t specify these arguments, we can infer that we must be in ‘local’ mode; and if we do provide them – then we must be in the ‘remote’ mode.
We could do this with an optional argument for remote mode, which then takes a pair of values – more or less as we did for the mutex group example.
parser.add_argument("--remote", '-r', nargs=2, help="Specify the tool to run in remote mode. Both a target address and port number must be specified",metavar=('TARGET', 'PORT'))
But, given that we’re again back to positional arguments, we could just as well just go all the way down that road, and back to a place where a positional argument makes sense.
For the target we’ll just use a simple positional argument, with the nargs
param set to ‘?
‘ (to demote 0 or 1). If it’s not supplied it’ll default to None
(as we saw before).
parser.add_argument("target", nargs='?', help="The address of a remote target, if required.")
For the port; we’ll take a different approach (although we could use the same approach as above, and check for None
ourselves)… Specifically we’ll provide a default port number – which can be used if nothing else is specified by the user.
parser.add_argument("port", nargs='?', default="1337", help="The port to use when connecting to the remote target. Port 1337 is assumed if a port is not otherwise specified.")
Now we just need to call the parse_args()
method.
args = parser.parse_args()
And finally we can check to see target
is None
, and then run our own logic.
Ifw we’d gone with the --remote
solution, this would look like:
if args.remote is not None: print(f"We're in remote mode: Target is {args.remote[0]}; port = {args.remote[1]}.") else: print("We're running in local mode.")
Otherwise (if we’re using the positional approach) we have a very slightly different filter…
if args.target is not None: print(f"We're in remote mode: Target is {args.target}; port = {args.port}.") else: print("We're running in local mode.")
Given the two final approaches, I think the --remote
one wins out for me in terms of elegance because we don’t need a default port value (and/or any other of our own code to check that a port number has been specified by the user); but there’s not really all that much to choose between them.
The final version of the code that I went with was as follows.
import argparse parser = argparse.ArgumentParser(description="Python Argparse demo") parser.add_argument("--remote", '-r', nargs=2, help="Specify the tool to run in remote mode. Both a target address and port number must be specified",metavar=('TARGET', 'PORT')) args = parser.parse_args() if args.remote is not None: print(f"We're in remote mode: Target is {args.remote[0]}; port = {args.remote[1]}.") else: print("We're running in local mode.")

Filed under: Python - @ June 11, 2022 17:55