command line interface with python and argparse

Supercharge Your Python Scripts With Command Line Interfaces

In this post, I’ll be showing you how to create a command-line interface (CLI) for your Python scripts. You will be able to run those scripts from the terminal with arguments.

Oftentimes, when you write a script, you need it to perform more than one action at a time. One way to provide this functionality is with a command-line interface (CLI) or menu. The straightforward way is by using the sys library. However, if you need more customization and control over the input, I suggest uisng the argparse library.

Argparse is part of the standard library, so you don’t need to install anything else. I will show a few examples of how to use it, and I’m also sharing a simple morse code translator script, just for fun.

If you like these types of posts, I have more under the programming tag on this blog. You can also subscribe to the newsletter list to get updated with new information, and to my Youtube channel for Python projects.

Argparse Library Vs Sys

You can create simple and basic command-line interfaces in Python using the sys library. However. with argparse you have finer control over the arguments passed. You can even do input-checking to ensure the user entered a valid value.

To illustrate the difference between the two libraries, I will write an example script with each of the two. Let’s say we are writing a script that takes a few parameters: name, age, and occupation.

Sys Example

Here is a silly example showing how you can use the sys library to read inputs from the command line. I created a file called sys_examples.py:

# sys_examples.py
import sys
import datetime as dt 


def main():
    name       = sys.argv[1]
    age        = int(sys.argv[2])
    occupation = sys.argv[3]

    print(f"Hello {name}.")
    print(f"Your age is {age}. Therefore, you were probably born around {(dt.datetime.today() - dt.timedelta(days=age*365)).year}.")
    print(f"Occupation is {occupation}.")


if __name__ == '__main__':
    main()

The way we run this script is from the terminal, like this:

python sys_examples.py Charlie 50 Astronaut

The result will be:

Hello Charlie.
Your age is 50. Therefore, you were probably born around 1972.
Occupation is Astronaut.

If we look back at the code, you can see where we access the parameters passed in the terminal (lines 7-9). The sys library gives us an array with the parameters passed from the command line.

That means we have to respect the order when running the script. Additionally, you will notice that we get the first argument, name, by accessing the array sys.argv[1]. This is because sys.argv[0] will contain the name of the script we are running, “sys_examples.py” , in this case.

Another important thing to point out is that all parameters in sys.argv will be strings. In the code above, we had to convert the age parameter to an int to process it correctly.

Argparse Example

I will recreate the example above using the argparse library in a file called argparse_examples.py:

import argparse
import datetime as dt


def cli():
    parser = argparse.ArgumentParser(description='Simple Example')
    parser.add_argument('-n', '--name', type=str, default='John', help="Name of user")
    parser.add_argument('-a', '--age', type=int, default=31, help="Age of user")
    parser.add_argument('-o', '--occupation', type=str, default='software developer', help='Occupation of user')
    
    args = parser.parse_args()
    return args


def main(args):
    name       = args.name
    age        = args.age 
    occupation = args.occupation

    print(f"\nHello {name}.")
    print(f"Your age is {age}. Therefore, you were probably born around {(dt.datetime.today() - dt.timedelta(days=age*365)).year}.")
    print(f"Occupation is {occupation}.\n")


if __name__ == "__main__":
    args = cli()
    main(args)

The first thing to notice is that we created a separate function, cli(), to host the command-line interface (CLI). I like to separate the CLI like this so that my code is more modular.

The next part is creating an argument parser with argparse.ArgumentParser. We can add a title and description for our script.

Next, we add all the parameters we want. Argparse allows us to specify the data type for each input, so the conversion happens automatically. Additionally, we can set a default value in case the user does not provide one. Moreover, one of the main benefits of using argparse is that we can have named arguments, including their shortcuts. For instance, for the name argument, I used “–name”.The user can also decide to use the shortcut version: “-n”.

Finally, we can write a short description (with the help parameter) for each argument. Argparse will use this description to automatically build a help menu that we can access from the terminal.

Now we can run the script

python argparse_examples.py --age 30 -o astronaut -n Charlie

Notice how we can pass arguments to the script in whatever order we want, as long as we provide the correct names for the arguments. In the example, I’m mixing both the full name and the shortcuts. However, inside the code, we will still access those parameters by their full name.

You can also use the equal ‘=’ sign to pass values to the parameters:

python argparse_examples.py --age=30 -o=astronaut -n=Charlie

Help Menu

On a final note, as I mentioned above, argparse will automatically create a help menu, which we can read from the terminal:

python argparse_examples.py --help

and the output will look like this:

usage: argparse_examples.py [-h] [-n NAME] [-a AGE] [-o OCCUPATION]

Simple Example

optional arguments:
  -h, --help            show this help message and exit
  -n NAME, --name NAME  Name of user
  -a AGE, --age AGE     Age of user
  -o OCCUPATION, --occupation OCCUPATION
                        Occupation of user

All the descriptions we wrote with the help parameter are used to show the user how to run the script.

More Argument Types With Argparse

Now I will mention other argument types that argparse supports.

Boolean Flags And Actions

Boolean flags are very useful when we want to activate a certain behavior that takes no additional value. For example, many command-line programs have a “–verbose” or “-v” parameter that we can pass. They don’t take any value. Just the fact that they are there tells the program to include more output to the terminal. We can do that with argparse in this way:

...
parser.add_argument('-v', '--verbose', action='store_true')
...

Here we are using the action parameter and setting it to “store_true” to indicate that this argument will be parsed as True if provided. We can also use “store_false” to set the value as False.

However, the action parameter can be used for more than just boolean flags. Here is the full documentation. Some of the options that it can be set to are:

  • “store_true” / “store_false”
  • “store_const”
  • “append”
  • “append_const”
  • “count”
  • “version”
  • “extend”

Read the documentation to learn how to use the other options. One that I have used many times is count, for the verbose parameter. The documentation includes that exact example:

...
parser.add_argument('--verbose', '-v', action='count', default=0)
...

So then when we run the script, we can indicate the level of verbosity we want. If we use “-v” then the value will be 1. However, if we do “-vv” the value will be 2, and so on.

Multiple Values With Nargs

In cases when we need to provide several values to the same command-line flag, we can use the nargs parameter. The documentation covers in detail how to use it, so I will only show a couple of things. The important thing to remember is that the values passed will be captured as an array (a Python list).

First, with nargs, you can specify exactly how many parameters the user can pass:

...
parser.add_argument('--coordinates', type=float, nargs=2,)
...

Now if we run the script, the program will throw an error message if it gets more or less than 2 values for the coordinates flag.

On the other hand, if we don’t want to specify a number, we can use nargs=’+’ or nargs=’?’. The difference between the two is that with ‘+’ the user must provide at least one value for the flag, whereas with ‘?’ the user can leave it empty. In both cases, the program will group any number of values provided, into the list.

Keep in mind that if you use nargs=’?’, then you should also include const, and default parameters as well. Here is an example from the documentation:

...
parser.add_argument('--foo', nargs='?', const='c', default='d')
...

In this example, if the user runs the script and does not include –foo at all, then the default value (‘d’) will be used. On the other hand, if the user does include the –foo flag but does not provide any value, then the const value (‘c’) is used.

Select From Pre-Defined Choices

If we want the user to choose from among a list of pre-defined valid options, we use the parameter choices. We need to pass a list of valid inputs. To illustrate:

...
parser.add_argument('--trading-pair', type=str, defautl='BTC-ETH', choices=['BTC-ETH', 'BTC-USD', 'ETH-USD', 'ADA-USD'])
...

This could be an example of a trading bot that launched from a Python script. We are giving the user different valid options to choose from: ‘BTC-ETH’, ‘BTC-USD’, ‘ETH-USD’, ‘ADA-USD’. If the user provides an input that does not match any of those options, then the program will throw an error.

Sub-Commands

I prefer to expand on sub-commands in a later post, but you should know that they can be used to create sub-menus. For example, when running git commands in the terminal, some arguments have their own sub-menus, such as git commit.

Check the documentation to make better sense of how to use it.

Morse Code Converter With Argparse

I wanted to write a fun and quick script using argparse, so I decided to write a simple morse code converter.

My script can take either text directly from the command line, or from a file, and convert it. The conversion can happen from text to morse code or vice versa. I used the International Radiotelephony Spelling Alphabet (NATO alphabet) as a reference for the translation.

It is a very rough script and it does not cover periods or commas. I’m thinking that it will be fun to turn this mini-project into a full-fledged Python package. That way, I can write a tutorial on that in the future. Anyways, here is the code:

import argparse
import nato_alphabet as nt


def cli():
    parser = argparse.ArgumentParser(description='Morse Code Tool')
    parser.add_argument('-s', '--source', type=str, default=None, 
                        help="File containing the text to translate.")

    parser.add_argument('-t', '--text', type=str, default='', 
                        help="""Enter text to translate directly. If `--source`
                                is provided, then this option (`--text`) is ignored.""")
    
    parser.add_argument('-m', '--mode', type=str, choices=['morse','telephony'], default='morse', 
                        help="""Choose mode of operation: morse or telephony.""")

    parser.add_argument('-d', '--direction', type=str, choices=['from_text', 'to_text'], default='from_text', 
                        help="""Select the direction of the convertion. 
                                If the source text needs to be converted to 
                                morse code, then choose `from_text` (this is the default)
                                and if the source text is already morse code
                                and you want to decrypt it, choose `to_text`.""")

    parser.add_argument('-o','--output', type=str, default=None, 
                        help="""File where to save the output. If this flag is not
                                provided, the output will be printed to the terminal.""")
                                
    args = parser.parse_args()
    return args


def main(args):
    #-- Get the source text to translate
    text = ""
    if args.source:
        with open(args.source, 'r') as file_handler:
            text = file_handler.read()

    else:
        text = args.text

    text = text.lower()

    #-- get mode and direction of the conversion, and output file
    mode      = args.mode
    direction = args.direction
    out_file = args.output

    #-- Convert text
    output_str = ''
    if direction == 'from_text':
        if mode == 'morse':
            for letter in text:
                output_str += nt.MORSE_CODE_MAPPER.get(letter, '*') + '^'
        else:
            for letter in text:
                output_str += nt.TELEPHONY_MAPPER.get(letter, '*') + '^'

    elif direction == 'to_text':
        text = text.strip('^').strip()
        if mode == 'morse':
            print(text.split('^'))
            for letter in text.split('^'):
                output_str += nt.REVERSE_CODE_MAPPER.get(letter, '*')
        else:
            for letter in text:
                output_str += nt.REVERSE_TELEPHONY_MAPPER.get(letter, '*')

    #-- clean up the result a little bit
    output_str = output_str.strip('^').strip(' ')

    #-- write result to file or to the terminal
    if out_file:
        with open(out_file, 'w') as file_handler:
            file_handler.write(output_str)
    else:
        print(output_str)


if __name__ == '__main__':
    args = cli()
    main(args)

The whole repository is in my GitHub repo, where you can study it more conveniently.

Some Troubleshooting

One issue that you might run into sometimes is when the value you are passing starts with ‘-‘. For example, if we run the morse code script above like this:

python main.py -t "---^...^-.-.^.-^.-." -d to_text

the program will throw an error like the following:

usage: main.py [-h] [-s SOURCE] [-t TEXT] [-m {morse,telephony}] [-d {from_text,to_text}] [-o OUTPUT]
main.py: error: argument -t/--text: expected one argument

The error is due to how argparse parses the input. Since the value we are passing to –text starts with a hyphen (‘-‘), the argparse parser assumes it is a new flag and not the value for the previous one.

Unfortunately, I could not find a simple way to fix that directly in the code. The simplest way to deal with this issue is to use the ‘=’ sign:

python main.py -t="---^...^-.-.^.-^.-." -d to_text

Now the program will understand the input and return:

sh-5.1$ python main.py -t="---^...^-.-.^.-^.-." -d to_text
['---', '...', '-.-.', '.-', '.-.']
oscar

Final Thoughts

I hope that this introduction to argparse is useful and can help you create powerful command-line interfaces. Try it and add more functionality to your Python scripts. Remember to take a look at the repository for this post. Additionally, here is another project about computer vision, where I also use the argparse library.

Let me know if there are any questions about the code or the post. Finally, if you like this kind of content, consider subscribing to my email newsletter to get updates on projects and posts.

Have anything in mind?