An analysis of several Python CLI libraries to detect their usability in various cases.
Usecase
We want to design a command with following usage based on compilation of several existing commands:
Usage: testcli [options] [--choices=VALUE]... [--params=...] create <label> testcli [options] [--choices=VALUE]... [--params=...] drop <label> testcli -h | --help testcli --version Options: -h, --help Show this help message and exit. --version Show program's version number and exit. --config=FILE Set config file. --string=STRING Set custom string. --number=NUMBER Set custom number. --params=KEY=VALUE Set custom parameters. --today=DATE Set custom today [default: today]. --choices=VALUE Set custom choices [default: all]. Available options: 'all', 'foo', 'bar', 'baz'. -n, --dry-run Don't actually do anything. -v,--verbosity=LEVEL Set verbosity level in range 0 to 3 [default: 1].
The command should provide two actions create and drop (I intentionally avoid term subcommand). Both actions require a label to act upon. The command also accepts a configuration file with three possible settings: string, number and params. All of the settings have their global defaults and all of them can be changed via the command options. Setting params is mapping of key-value settings. Other options are used to change current day --today and select --choices. Choices are represented by a Choice enumeration in code, with a value ALL as a shortcut for all other. Common options --dry-run and --verbosity are also required.
Analyzed libraries:
Results
Shared code is placed in testcli.py. Individual scripts then contain only the code for parsing and processing the command line arguments.
argparse
argparse is part of standard library, so no external dependecies are required. That may be in some cases beneficial, although rarely.
The crucial part of implemntation is a creation of the parser instance. For argparse that’s usually a tedious and quite difficult, especially in less common cases. A connection with a config file was also uneasy. ArgumentParser provides an option to set a per-option default values, but later update with configuration file requires a second parsing of command line arguments. Setting a default for repeated option --choices is also somewhat clumsy. All provided options are appended to a default if it isn’t None, instead of overwriting it, which I would expect. Slight complication is that parsed arguments are returned in an object, not in a dictionary, which would usually be more useful.
Feature summary:
- Converts arguments.
- Allows custom parameter types.
- Is easily extensible.
Whole implementation: testcli_argparse.py
docopt
docopt may be the simplest way to go. docopt is based on reversed approach than traditional CLI libraries. Instead of structured definition of individual options, that are the used to generate usage, docopt uses the usage description and parses it according to POSIX standard. The downside of this approach it that docopt is purely string based and appart from flag switches all parameters must be converted additionally.
The main part of implementation is writing a valid usage pattern, which is actually after few attempts quite easy. The other main part is conversion of parameters which are not strings or booleans into correct types, either in custom way, or using other tools, such as pydantic. Being quite simple library there aren’t many things that may go wrong, apart from correctly defining usage itself. I only encountered unsolved problem with multiplying values for options which may be provided multiple times: https://github.com/docopt/docopt/issues/134. Parsed arguments are returned as a dictionary, where keys contains also POSIX chars, such as <label> or --dry-run. That’s something that one needs to adapt to. Also all errors from value conversions must be handled, otherwise the command quits with traceback.
Since the whole parsing is based on usage string, it would be quite complicated to share a group of options among several related commands. It’s just better to copy the usage in each of them.
Feature summary:
- Doesn’t convert arguments.
- Is not extensible.
Whole implementation: testcli_docopt.py
click
click is based on completely different approach then argparse or docopt. It tries to bridge a gap between the command line and function arguments.
The main part of implementation are the actual functions that perform the required command. Functions are annotated by decorators to provide click information about the individual parameters. click provides a hierarchical structure of subcommands, with very strict order of arguments. For example, having command with --dry-run option and subcommand
# This will work python3 example.py command --dry-run subcommand # This will fail python3 example.py command subcommand --dry-run
This reduced ambiguity on argument parsing, but may cause somewhat unexpected behaviour in cases where it is unclear if positional argument is a subcommand or not.
Feature summary:
- Converts arguments.
- Allows custom parameter types.
- Is easily extensible.
- Is very strict about the order of parameters in the command line.
Whole implementation: testcli_click.py
cleo
cleo seems to be a somewhat mix between docopt and click. Maybe I didn’t quite get it, but it seems either of them would be a better fit in any case. Individual commands are defined as classes, which may be beneficial in some cases, but arguments are described in specific language with quite limited documentation.
The implementation doesn’t differ from the one for docopt much. Sadly, I haven’t found a way to write both actions as a single command, so the usage patterns are duplicated even in this case. Similarly to docopt there aren’t many things that could go wrong, but definitions of options are less documented and less clear than in docopt.
Feature summary:
- Doesn’t convert arguments.
- Is not extensible.
Whole implementation: testcli_cleo.py
typer
typer is actually a framework over the click. As its name hints, it is based on typing annotations, which are used to define the command line interface.
As a result, the implemetation doesn’t quite differ from the implementation for click, but instead of decorators, options are defined as annotation. I find that quite confusing. In case of well defined interface, the annotation turn rather quickly into very complex structure which completely obscures its original purpose. It doesn’t provide a support for --version option, which a downside for any versioned script.
Feature summary:
- Converts arguments.
- Allows custom parameter types.
- Is not extensible.
- Doesn’t provide --version option.
Whole implementation: testcli_typer.py
plac
plac is actually a framework over argparse. Similarly to click, it tries to ease the transition between command line parameters and function arguments. Sadly, it uses some magic overriding function annotations, causing errors when applied to type annotated, but not plac annotated functions, although it should work according to documentation.
Similarly to click, the main part of implementation is the actual function and its decorators which provide additional information for a parser. But it has several serious downsides. I haven’t found a way to define an option which could be repeated. It provides an automatic short option for every option with no default conflict resolution. That requires a manual resolution of a conflict every time a two options start with the same letter. It hardcodes -v as a short option for --version with no apparent way to override it. That makes -v completly unusable for –verbosity a may lead to errors when used, since this way is more common. Also I haven’t found a way to avoid the @plac.annotations() decorator. It just doesn’t work without it.
Feature summary:
- Converts arguments.
- Allows custom parameter types.
- Is easily extensible.
- Some features are not available.
- Hardcodes -v to --version option.
Whole implementation: testcli_plac.py
Summary
argparse is a standard, but not a golden one. It can be used for very simple cases, but everyone I know, always fights with the parser definition for way too long. docopt is a nice quick and dirty solution for a simple scripts. Aside of the bug I found, there isn’t much that could go wrong. Although it’s very strict about the order of parameters, click looks like a solid solution for complex CLI applications. cleo seems it could be easily replaced by either docopt or click. I haven’t quite found a reasonable way to use it. Use of annotations by typer heads into a right direction, but proper implementaion makes code illegible. It might serve well as quick and dirty solution to provide a CLI for an existing function with no documentation. plac just looks unusable.
I personally love docopt but its last release was 6 years ago so I’m looking at modern alternatives and this summary was helpful.
According to my brief LibHunt research, these are the top 3 CLI python projects:
1) fire – 19k ★ (not mentioned? 🤔)
2) click – 10k ★
3) docopt – 7k ★
Honorable mention goes to typer at 5k ★ which is a big number given it requires relatively new python 3.6+.
Thanks for the feedback. Fire project seems to somehow slip through.
Hopefully, docopt will be renewed.