Tutorial: Create a Python CLI Package via Traitlets | by Joél Collins | Towards Data Science

If you’re like me...
You love writing scripts to speed up your workflow
You’re fluent in Python and basically nothing else.
Annoyingly, python can feel a little clunky in the terminal.
If you want to run python scripts from command line, you either need to...
Assign a different command (usually a bash alias) to every script impossible to remember
Use the sys python package to parse the command line arguments from scratch gross
Use argparse to parse the command line, and activate every function with if/else checks. also gross
Fortunately, there is a solution to this issue! It’s called traitlets.
Traitlets was created by IPython to validate datatypes, monitor changes to variables, parse and implement application settings from configuration files, and activate python code from the command line. It functions as a sort of behind the scenes engine for much of the software produced by IPython and the Jupyter Project. For now, we will focus on two elements of traitlets.
Activating python code from the command lineConfigurable variables
Activating python code from the command line
Configurable variables
In this article, I will walk through building a simple traitlets application. With this knowledge you will be able to write a command line program that
Is written entirely in python.
Assigns multiple tools to a single CLI command.
Can be easily configured via command line or in system configuration files. (You do not have to write code to parse configuration data 🤩)
Has a straight forward Object Oriented Programming structure.
The project code:
If you want to write the project code yourself, this github repository contains all of the project files. The code in each file has been replaced with guiding comments.
The completed project code can be found here. (The code is stored on the the solution branch of the github repository. If you’ve cloned the repository, you can run git checkout solution to access the code on your computer.)
First let’s take a look at what the finished product looks like...
This application has two commands.
greet
When this command is run “Hello, World!” is printed to the terminal.
This command has the arguments --greeting, --name, and --punctuation that allow you to alter the printed message.
abs
When this command is run the absolute path for a provided filename is printed to the terminal.
This command has the flag --size. When this flag is used, the file size (in bytes) is printed in addition to the absolute path.
The directory structure for this project looks like this:
subapps/ <------------- All "command" code-files are placed here     AbsApp.py  <------ Code for the `abs` command     GreetingApp.py <-- Code for the `greet` command     __init__.py <----- Set up for easy importingCLIapp.py <------------ Code for CLI activation
okok let’s write some code.
Navigate into the subapps/directory of the project folder and open the GreetApp.py file.
We will need two imports for this file.
from traitlets.config import Applicationfrom traitlets import Unicode
from traitlets.config import Application
from traitlets import Unicode
Before moving on, let’s quickly break down these two imports.
Import #1 — Application
Application is the “parent” for every class we will write in this project. Applicationobjects offer a slew of helpful tools such as parsing the command line, loading configuration files, generating config files, auto-generating code documentation, and more. In this tutorial we will focus on the methods used by Application objects to parse the command line and activate the application code.
Import #2 — Unicode
Unicode is a traitlets type.
Code examples of types.
List of all available types.
Traitlets types are class attributes that, when defined, prevent attributes from being assigned to the incorrect datatype. When the argument config=True is added to the type, the variable becomes configurable, which means the default value for the attribute can be changed via command line arguments or by setting the desired default inside a configuration file.
In the code cell below we...
Import theApplication and Unicode objectsDefine the GreetApp classInherit from the Application parent classDefine type variables with a default setting.Mark the variable as configurable
Import theApplication and Unicode objects
Define the GreetApp class
Inherit from the Application parent class
Define type variables with a default setting.
Mark the variable as configurable
Now let’s create the start method
The start method is where the app’s functionality is activated.
In the code block below we...
Define a start methodActivate the app functionality
Define a start method
Activate the app functionality
Technically, we could leave the app like this and it would work, but changing the configurable variable would be a little annoying for the user.
Right now, instead of users running --greeting='Hi' to change the greeting config variable to 'Hi' , the user would have to run:
--GreetApp.greeting='Hi'
To make this easier, we have to create an aliases class attribute. This attribute is used by the Application parent class while parsing the command line.
In the code block below we...
Define a dictionary containing our aliasesSet the dictionary keys to the command line arguments we want to useSet the value to the name of the Application child we want to configureClarify the configurable variable we want to alterSet the aliases class trait to the dictionary of aliases
Define a dictionary containing our aliases
Set the dictionary keys to the command line arguments we want to use
Set the value to the name of the Application child we want to configure
Clarify the configurable variable we want to alter
Set the aliases class trait to the dictionary of aliases
The GreetApp is done!
There are a few other things we can add to this app that will be covered in a follow up article “How to Autogenerate Documentation.” If you’re working through the code-it-yourself files, skip the unfilled out portions for now. They are not required for this article.
Now that we’ve completed an application, we need to set up the main function that activates the code. This process will be handled by the CLIapp.py file.
Navigate to the CLIapp.py file.
For this file, we will need the following imports.
from traitlets.config import Applicationfrom subapps import GreetApp (The application object we just wrote!)
from traitlets.config import Application
from subapps import GreetApp (The application object we just wrote!)
Application objects have an attribute called subcommands that allow you to attach commands for activating specific applications. In this file we will define an application called CLIapp that contains a dictionary of subcommands, and a start method for activating the subcommand’s start method. TL;DR The CLIapp is an application with the sole purpose of activating the code for other applications.
Let’s write this out
In the code block below we...
Import the Application and GreetApp objectsDefine the CLIPapp applicationDefine a dictionary with the following format:
Import the Application and GreetApp objects
Define the CLIPapp application
Define a dictionary with the following format:
{command : (application object, application description)}
4. Define a startmethod
5. Check to make sure the user has provided a subcommand. If they haven’t print the available subcommands
6. Activate the Application.start method which in turn, activates the subapplication’s start method.
Ok! That is the CLIapp
Now the last thing we need to do is define a main function that activate all the code.
In the code block below we...
Define a main function outside of the CLIappActivate the code via the launch_instance method. This is where the command line is parsedAdd if __name__ == "__main__" check which allows us to call the main function from command line.
Define a main function outside of the CLIapp
Activate the code via the launch_instance method. This is where the command line is parsed
Add if __name__ == "__main__" check which allows us to call the main function from command line.
We now have a working application! 🥳
In your terminal, navigate to the top level of the project directory and run
python CLIapp.py greet “Hello, World!” will be printed
python CLIapp.py greet --greeting="Hi" “Hi, World!” will be printed
python CLIapp.py greet --name="Joel" “Hello, Joel!” will be printed
Now let’s look at the process for adding new tools to an existing application by creating the abs command.
Inside the AbsApp.py file, we will run the following imports
from traitlets.config import Applicationfrom traitlets import Boolimport os
The two new imports are Bool and os .
Bool is another traitlets type. In this case, it ensures that the variable is set to either True or False
os is a base Python package that stands for “Operating System”. It is a standard package used to generate file paths, to list files inside a folder, and to interact with a computer’s operating system. In our case, we will use os to generate the absolute path for a file and to calculate how many bytes a file uses.
Ok, next we will construct our AbApp class.
In the cell below we...
Define the AbsApp applicationDefine a configurablesize variable with a traitlets type of BoolDefine a start methodAccess the filename that has been provided by the user via the extra_args attributeEnsure the file provided by the user is a valid file via the os isfile function.Print the absolute path for the provided filenameIf the user has set the size configurable variable to True the file-size is also printed.
Define the AbsApp application
Define a configurablesize variable with a traitlets type of Bool
Define a start method
Access the filename that has been provided by the user via the extra_args attribute
Ensure the file provided by the user is a valid file via the os isfile function.
Print the absolute path for the provided filename
If the user has set the size configurable variable to True the file-size is also printed.
Now, we will create a flag for the AbsApp that allows users to simply run...
--size
...without having to explicitly set size to True or False. To do this we will define a flags class variable that is used by the Application parent class while parsing the command line.
In the cell below we...
Define a dictvariable called flagsSet the top level key to flag that will be used in command lineSet the value of the key to a tupleAdd a dictionary to the tuple where the key is the name of the application that should be configured by the flagSet the value of the key to another dictionary where the key is the name of the configurable variable, and the value is the data would like to assign to the variable.Add a description for the flag as the second item inside the tuple. This will be printed if a user uses the --help argument!Add the flag to the application via the Application.flags attribute.
Define a dictvariable called flags
Set the top level key to flag that will be used in command line
Set the value of the key to a tuple
Add a dictionary to the tuple where the key is the name of the application that should be configured by the flag
Set the value of the key to another dictionary where the key is the name of the configurable variable, and the value is the data would like to assign to the variable.
Add a description for the flag as the second item inside the tuple. This will be printed if a user uses the --help argument!
Add the flag to the application via the Application.flags attribute.
Worth noting: flags are pretty powerful when it comes to updating configurations. The second level dictionary can contain multiple applications and the third level dictionary can contain multiple configurable variables!
The AbsApp is complete! ✅
Now the final step is to add the app to the CLIapp.py file!
In the code block below we...
Import the AbsApp object into the CLIapp.py fileAdd an abs subcommand to the CLIapp object
Import the AbsApp object into the CLIapp.py file
Add an abs subcommand to the CLIapp object
And just like that, a new command is added to your application!
In your terminal, navigate to the top level of the project directory and run
python CLIapp.py abs CLIapp.py The absolute path for the CLIapp.py file will be printed
python CLIapp.py abs CLIapp.py --size The absolute path and the filesize for the CLIapp.py file will be printed
In my own experience writing applications with traitlets, the ease of writing new applications and adding new commands to an existing tool is one of the best perks. Because the process is so rooted in Object Oriented Programming, sometimes writing an entirely new application is as simple as inheriting from an existing application and then editing a single line of code!
Alright, that is it for now. Be sure to click the follow button for my next article: A walkthrough of traitlets configuration files and how to autogenerate code documentation!