Fire: Simple CLIs done right

A few months ago, I had an enthusiastic outburst in which I expressed my appreciation for a little package called TQDM for creating progress bars. This post is in the same vein but this time for Fire: a great package that can make getting a Command Line Interfaces (CLI) up and running in (literally) a couple of seconds a breeze.

So, why write a CLI? Practically, a simple CLI can make configuring a script as simple as changing a couple of command line arguments. Let's say you've got a script set up on an orchestration service (maybe something like Jenkins) that regularly retrains your latest and greatest Tweet sentiment classifier. Let's say it's a Scikit-Learn Random Forest. You might run the job as something like:

python tweet-classifier/train.py

You might choose to tweak the model's parameters directly in the code. A better approach would be to have the model encapsulated in a function, and to have the model's parameters bound to a function signature (e.g. train(n_estimators: int = 100)) and instead run something like:

python tweet-classifier/train.py --n_estimators 100

This would allow you to configure the model's hyperparameters through the CLI.

This prevents you from needing to make changes to the code itself in order to change the script's configuration, and this in turn can help other users pick up and run your code with minimal fuss. This is also typically a much better idea for 'production' code, where changes to source code should be carefully tracked and propagated to all instances of that code (you might be running dozens of distinct Tweet classifiers, and changing the code for one might change them all in unexpected ways). Dozens of stale Git branches are a bad solution to this too. Ad hoc changes to deployed code can make quite a mess and ultimately cost you (or your employer) a good deal of money. CLIs can be a tool in your toolbox to help you avoid this fate.

While it is possible (and still quite straightforward) to setup a CLI using Python's Standard Library, it can get verbose pretty quickly. Here's a simple example of how you might make a CLI for a script that prints the Fibonacci sequence up to n:

# fib.py
import argparse

def fibonacci(n, a=0, b=1):
    """Print the Fibonacci series up to n."""
    while a <= n:
        print(a)
        a, b = b, a + b

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-n", type=int)
    args = parser.parse_args()
    fibonacci(args.n)

Now, to execute this you can run:

python fib.py -n 13	

You should see the Fibonacci sequence printed to console. Simple enough. Now let's say you want to set your initial conditions a and b too. You could do this with:

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-n", type=int)
    parser.add_argument("-a", type=int)
    parser.add_argument("-b", type=int)
    args = parser.parse_args()
    fibonacci(args.n, a=args.a, b=args.b)

Still quite simple, but your code is rapidly becoming a bit bloated. You might be able to imagine that as you increase the number of parameters in our function signature (or add new functions), our ArgumentParser block is going to grow rapidly and keeping track of all these functions and their arguments can get both fiddly and can end up constituting many dozens of lines of code. Plus, for simple scripts, this might be enough of a barrier that adding a CLI might seem like a bit of pain – maybe you might even be tempted to change some parameters directly in the script.

Enter Fire

That's where Fire comes in: it automatically generates CLIs from function/method signatures. Let's unpack that by looking at the original example, but this time using Fire:

# fib.py
import fire

def fibonacci(n, a=0, b=1):
    """Print the Fibonacci series up to n."""
    while a <= n:
        print(a)
        a, b = b, a + b

if __name__ == "__main__":
    fire.Fire(fibonacci)

First up, you can install Fire as a normal Python package with pip install fire. There's more installation details over on Fire's project repo. You can now run:

python fib.py -n 13

You should see the same output. You can also run:

python fib.py -n 13 -a 0 -b 1

As you can see, Fire automatically maps the arguments in the function signature (i.e. function parameters) to corresponding CLI arguments. In only a single line you've got your CLI up and running. But this is only a simple case. Let's assume you want to create a CLI for a script with lots of different functions (maybe one for training a model, one for running inference etc.). Here's a modified version of the earlier example that introduces a new function and a new tuple object struct:

# cli.py
import fire

def fibonacci(n, a=0, b=1):
    """Print the Fibonacci series up to n."""
    while a <= n:
        print(a)
        a, b = b, a + b

def say_hello(name):
    print(f"Hello, {name}!")

struct = tuple(["a", "b"])

if __name__ == "__main__":
    fire.Fire()

This does something quite interesting. If you leave the first argument of the Fire function call empty, Fire instead inspects the current module for Python objects and exposes them via a CLI. For example, you can run:

python cli.py fibonacci -n 13

This will run the fibonacci function as before. However you can also run:

python cli.py say_hello Jane

This will produce the output Hello, Jane! as expected. Perhaps most interestingly, you can also run:

python cli.py struct 0

You should see the output a. Similarly, you can run:

python cli.py struct 1

And should see b. In this case Fire is letting you interact directly with a native Python object directly from the CLI. You can extend this idea to your own custom data structures too.

Closing thoughts

Fire is a great tool for getting clean, good-quality CLIs up and running in seconds, and it's so simple that you barely need to learn any new concepts to use it in your work. It's ideal to quickly wrap a new model pipeline in a shiny new CLI ready for deployment. There's also a bunch of more advanced features you can make use of to build out more elaborate CLIs too.

google/python-fire
Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object. - google/python-fire

However, if you're building a fully featured CLI and want fine-grained control, you might want to look into the venerable and super-flexible click library. That'll give you as much control as you could reasonably want (in Python at least).

pallets/click
Python composable command line interface toolkit. Contribute to pallets/click development by creating an account on GitHub.