Creating python command-line applications

For those who comes from .NET or Java, one of best things about python or ruby is the capability to write scripts. These little programs can solve a lot of problems like move files in a CI Server, clean some data, run tests and so on. When we are learning python, we have to learn also to use python scripts, everybody uses the following commands when we are installing a python library:

python setup.py build
python setup.py install

Or we use the following command when we are programming with django:

django-admin.py startproject mysite

Building a command-line application

In this post, we are going to learn how we can build a command-line calculator. Below is our application backlog:

/*
We are going to create a command-line calculator. The calculator should be used in this way:

python calc.py add 2 2
> 4

python calc.py subtract 2 2
> 0

python calc.py multiply 2 2
> 4

python calc.py divide 2 2
> 1
   
*/

Using sys.argv

When calling a python script, our arguments are stored in sys.argv variable in the following order: Script Name + arguments, so we can print each argument in this way:

# calc.py

import sys

for arg in sys.argv:
	print arg

And here we are printing our arguments:

python calc.py 1 2 3
> calc.py
> 1
> 2
> 3

Building our calculator

We already know how we can get our arguments, now we can implement the operations:

Implementing Add

# calc.py

import sys

args = sys.argv[1:]

operation = args[0]
num1 = int(args[1])
num2 = int(args[2])

if operation == 'add':
	total = num1 + num2

print total	

Other Operations

Implement the other operations is straightforward, we can easily code them:

# calc.py

import sys

args = sys.argv[1:]

operation = args[0]
num1 = int(args[1])
num2 = int(args[2])

if operation == 'add':
	total = num1 + num2
elif operation == 'subtract':
	total = num1 - num2
elif operation == 'multiply':
	total = num1 * num2
elif operation == 'divide':
	total = num1 / num2

print total

A switch..case situation ?

Python, unlike many OO languages, doesn’t have switch..case statement and if it can be seen as a disadvantage at first look, it has a reason! When we use switch..case, in most of cases it indicates that there should be a polymorphic call there. But if..elif is bad as switch..case. We can write it in a more elegant way using dictionaries and functional programming. So we end up with the following code:

# calc.py

import sys

args = sys.argv[1:]

operation = args[0]
num1 = int(args[1])
num2 = int(args[2])

functions = {
	'add': lambda num1, num2: num1 + num2,
	'subtract': lambda num1, num2: num1 - num2,
	'multiply': lambda num1, num2: num1 * num2,
	'divide': lambda num1, num2: num1 / num2
}

total = functions[operation](num1, num2)

print total

Much better!

Just a toy application

It’s just a toy application. People don’t write command-line applications in this way. Real command-line applications have error checking, flags, help and other things. Instead it, people use modules that help to achieve these requirements. Some of the existent options are:

Using argparse to write a more robust application

Argparse makes easy to write command-line applications. You provide info about the parameters and argparse parse those out of sys.argv. It also provides help, usage messages and issues errors.

A basic program

import argparse

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

This program does nothing. But we can already see the error messages, help and usage tips.

python calc_argparse.py
>>>

python calc_argparse.py --help
>>> usage: calc_argparse.py [-h]
>>>
>>> optional arguments: -h, --help  show this help message and exit

python calc_argparse.py --flag
>>> usage: calc_argparse.py [-h]
>>> calc_argparse.py: error: unrecognized arguments: --flag

python calc_argparse.py arg
>>> usage: calc_argparse.py [-h]
>>> calc_argparse.py: error: unrecognized arguments: arg

Adding arguments

Our command-line calculator does nothing, so let’s start to add the parameters. The first parameter will be the operation.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("operation")
args = parser.parse_args()

print args.operation

Let’s see how argparse parses arguments and shows error and help messages.

python calc_argparse.py
>>> usage: calc_argparse.py [-h] operation
>>> calc_argparse.py: error: too few arguments

python calc_argparse.py --help
>>> usage: calc_argparse.py [-h] operation
>>> positional arguments: operation
>>> optional arguments: -h, --help  show this help message and exit

python calc_argparse.py sum
>>> sum

Better help messages

Everything is working fine. But if we look carefully, what operation means ? We should have a help message. With argparse we can do it when we add an argument. Let’s do it:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("operation", help="mathematical operation that will be performed")
args = parser.parse_args()

print args.operation

And we will see a better help message.

python calc_argparse.py --help
>>> usage: calc_argparse.py [-h] operation
>>> positional arguments: operation   mathematical operation that will be performed
>>> optional arguments: -h, --help  show this help message and exit

Write the calculator

Until this moment we are just playing with argparse features. Now, we are going to write our calculator operations.
We have to add more two arguments (num1 and num2) and we will use the functions from the previous example to perform the operations. It should work.

import argparse

functions = {
	'add': lambda num1, num2: num1 + num2,
	'subtract': lambda num1, num2: num1 - num2,
	'multiply': lambda num1, num2: num1 * num2,
	'divide': lambda num1, num2: num1 / num2
}

parser = argparse.ArgumentParser()
parser.add_argument("operation", help="mathematical operation that will be performed")
parser.add_argument("num1", help="the first number")
parser.add_argument("num2", help="the second number")
args = parser.parse_args()
print functions[args.operation](args.num1, args.num2)
python calc_argparse.py multiply 1 2
>>> Traceback (most recent call last):
>>>  File "calc_argparse.py", line 16, in <module>
>>>    print functions[args.operation](args.num1, args.num2)
>>>  File "calc_argparse.py", line 6, in <lambda>
>>>    'multiply': lambda num1, num2: num1 * num2,
>>> TypeError: can't multiply sequence by non-int of type 'str'

But it’s not working. If we analyze the error, we can perceive that looks like the numbers are been parsed as str. It’s a TypeError. When using argparse, we have to pass the argument types, it will parse with the right type and will check for type errors. Let’s do it.

import argparse

functions = {
	'add': lambda num1, num2: num1 + num2,
	'subtract': lambda num1, num2: num1 - num2,
	'multiply': lambda num1, num2: num1 * num2,
	'divide': lambda num1, num2: num1 / num2
}

parser = argparse.ArgumentParser()
parser.add_argument("operation", help="mathematical operation that will be performed")
parser.add_argument("num1", help="the first number", type=int)
parser.add_argument("num2", help="the second number", type=int)
args = parser.parse_args()

print functions[args.operation](args.num1, args.num2)

Now everything is working fine, we also have type errors checking.

python calc_argparse.py add 2 2
>>> 4

python calc_argparse.py subtract 2 2
>>> 0

python calc_argparse.py multiply 2 2
>>> 4

python calc_argparse.py divide 2 2
>>> 1

python calc_argparse.py add '2' '2'
>>> usage: calc_argparse.py [-h] operation num1 num2
>>> calc_argparse.py: error: argument num1: invalid int value: "'2'"

Operation Error Checking
Look what happens when me try to perform a invalid operation.

python calc_argparse.py invalid_operation 2 2
>>> Traceback (most recent call last):
>>>  File "calc_argparse.py", line 16, in <module>
>>>    print functions[args.operation](args.num1, args.num2)
>>> KeyError: 'invalid_operation'

Obviously, there isn’t a ‘invalid_operation’ operation in math. So we should be limiting the available operations. With argparse, we can do it using the choices argument when we are adding an argument:

import argparse

functions = {
	'add': lambda num1, num2: num1 + num2,
	'subtract': lambda num1, num2: num1 - num2,
	'multiply': lambda num1, num2: num1 * num2,
	'divide': lambda num1, num2: num1 / num2
}

parser = argparse.ArgumentParser()
parser.add_argument("operation", help="mathematical operation that will be performed", 
	choices=['add', 'subtract', 'multiply', 'divide'])
parser.add_argument("num1", help="the first number", type=int)
parser.add_argument("num2", help="the second number", type=int)
args = parser.parse_args()

print functions[args.operation](args.num1, args.num2)

Now, if we try a different operation we will see a better error.

python calc_argparse.py invalid_operation 2 2
>>> usage: calc_argparse.py [-h] {add,subtract,multiply,divide} num1 num2
>>> calc_argparse.py: error: argument operation: invalid choice: 'invalid_operation'
>>>  (choose from 'add', 'subtract', 'multiply', 'divide')

This is it. With argparse, we can achieve a more robust and error-safe application with less code.

5 Comments

  1. Comment by Jakob:

    Or you could teach someone something useful like idk, argparse.

  2. Ping from Creating python command line applications with docopt:

    [...] Creating python command-line applications [...]

Leave a Reply

Your email address will not be published. Required fields are marked *