Python makes it easy to create command-line interfaces (CLI). We will compare the three popular libraries argparse, docopt and click. They all take a very different approach to the problem.

As an example, we implement a function that downloads stock prices for a list of symbols (e.g. AMZN for Amazon) from Google Finance and stores them into a csv-file. There are three optional parameters start_date, end_date to slice the timeseries, and file_name to specify the name of the output file.

from datetime import datetime as dt

import pandas as pd
import pandas_datareader.data as web


def store_stock_prices(symbols, start_date=None, end_date=None, file_name='stockprices.csv'):
    df = pd.concat((web.get_data_google(symbol).Close for symbol in symbols), axis=1).ix[start_date:end_date]
    df.columns = symbols
    df.to_csv(file_name)

In order to handle the input of dates, we use a simple function that converts from strings of the format YYYY-MM-DD to datetime and throws an Exception for invalid inputs.

def valid_date(s):
    try:
        return dt.strptime(s, "%Y-%m-%d")
    except ValueError:
        msg = "Not a valid date: '{0}'.".format(s)
        raise Exception(msg)
    except TypeError:
        return None

argparse

argparse is part of the Python Standard Library. It replaces the deprecated predecessor optparse. It is rather verbose and declarative but it is more systematic than using sys.argv and avoids adding boiler plate code.

import argparse

from stock_price_utils import valid_date, store_stock_prices

parser = argparse.ArgumentParser()

parser.add_argument('SYMBOLS',
                   help="Comma separated list of stock symbols")
parser.add_argument('-s', "--start_date",
                   help="The Start Date - format YYYY-MM-DD",
                   required=False,
                   default=None,
                   type=valid_date)
parser.add_argument('-e', "--end_date",
                   help="The End Date - format YYYY-MM-DD",
                   required=False,
                   default=None,
                   type=valid_date)
parser.add_argument('-f', "--file_name",
                   help="The path to output file",
                   required=False,
                   default='stockprices.csv')

args = parser.parse_args()


if __name__ == '__main__':
   store_stock_prices(symbols=args.SYMBOLS.split(','),
                      start_date=args.start_date,
                      end_date=args.end_date,
                      file_name=args.file_name)

Advantages:

  • Does not require third party packages
  • Easy to debug

Disadvantages:

  • Verbose and hard to read

docopt

docopt takes the approach to define the CLI with the docstring. The idea is to have cleaner uninterrupted code and a easily-readable interface definition. It requires the docstring to be in a precise format. The passed arguments and options are returned as a dict and it doesn’t give much additional functionality in terms of type conversion and error handling.

"""Usage: docopt_example.py SYMBOLS [--start_date STARTDATE] [--end_date ENDDATE] [--file_name FILENAME]

Arguments:
  SYMBOLS                   Comma separated list of stock symbols
Options:
  --start_date=START_DATE   The Start Date - format YYYY-MM-DD
  --end_date=END_DATE       The End Date - format YYYY-MM-DD
  --file_name=FILE_NAME     The Path to output file
  -h --help
"""

from docopt import docopt
from stock_price_utils import valid_date, store_stock_prices


if __name__ == '__main__':
    arguments = docopt(__doc__)

    start_date = valid_date(arguments['--start_date']) if '--start_date' in arguments else None
    end_date = valid_date(arguments['--end_date']) if '--end_date' in arguments else None
    file_name = arguments['--file_name'] if '--file_name' in arguments else None

    store_stock_prices(symbols=arguments['SYMBOLS'].split(","),
                       start_date=start_date,
                       end_date=end_date,
                       file_name=file_name)

Advantages:

  • Readable interface definition
  • Moves the complexity of defining the CLI out of the actual code

Disadvantages:

  • None-handling and type conversion has to be implemented separately
  • Forces developer to specify docstring in certain format
  • Difficult to debug and test

click

Instead of defining a separate parser click provides a set of decorators to wrap the actual function and transform itself into a CLI. Thus, in this example the implementation of store_stock_prices is actually included.

import pandas as pd
import pandas_datareader.data as web
import click

from stock_price_utils import valid_date

split_parameter = lambda _, __, s: s.split(",")
check_date = lambda _, __, s: valid_date(s)


@click.command()
@click.argument('symbols', callback=split_parameter)
@click.option('--start_date', callback=check_date)
@click.option('--end_date', callback=check_date)
@click.option('--file_name')
def store_stock_prices(symbols, start_date, end_date, file_name):
    df = pd.concat((web.get_data_google(symbol).Close for symbol in symbols), axis=1).ix[start_date: end_date]
    df.columns = symbols
    df.to_csv(file_name)

if __name__ == '__main__':
    store_stock_prices()

Advantages:

  • Simple yet expressive decorator-based interface implementation

Disadvantages:

  • Decorated functions can’t be called with parameters anymore, would require implementing a thin wrapper