I remember the first time I tried to build a command-line tool. I had a Python script that did something useful—renaming a bunch of files—but every time I wanted to change a setting I had to edit the script. That got old fast. I wanted to pass arguments from the terminal, show a nice help message, maybe add some color. I didn’t know where to start. Over the years I learned that Python has a whole ecosystem of libraries for exactly this purpose. They take you from a bare‑bones script to a polished tool that feels like it was written in C. Let me walk you through the six that I use most often. I will show you simple code, explain why each one exists, and share the little tricks I picked up along the way.
The first library you will encounter is argparse. It comes built into Python, so you don’t need to install anything. When I started, I thought it was complicated. It isn’t. It just wants you to tell it what arguments your tool expects. You create a parser object, add arguments, and call parse_args(). That’s it.
Let me show you a real example. Suppose you want to write a tool that reads a list of numbers from a file and prints their sum. You want the user to specify the input file and an optional flag to show verbose output.
import argparse
parser = argparse.ArgumentParser(description='Sum numbers from a file.')
parser.add_argument('filename', help='path to the input file')
parser.add_argument('--verbose', '-v', action='store_true', help='show details')
args = parser.parse_args()
with open(args.filename) as f:
numbers = [float(line.strip()) for line in f if line.strip()]
total = sum(numbers)
if args.verbose:
print(f'Found {len(numbers)} numbers. Sum: {total}')
else:
print(total)
When you run python sum_tool.py data.txt -v, argparse turns the string data.txt into args.filename and sets args.verbose to True. It also gives you -h for free, which prints a help message. I love that argparse handles the boring stuff—type checking, error messages, default values—so I can focus on the logic.
A personal touch: I use argparse for every tool that I share with teammates. Nobody wants to remember the order of positional arguments. With argparse, you can define named flags like --output or --limit. The help message is always one command away. If you only learn one library for CLI tools, start here.
But argparse can feel verbose when your tool has many commands. That is where Click enters. Click uses decorators to turn functions into commands. You decorate a function with @click.command(), and then add @click.option() for each flag. The library handles the parsing and calls your function with the values.
Here is the same sum tool with Click:
import click
@click.command()
@click.argument('filename', type=click.Path(exists=True))
@click.option('--verbose', '-v', is_flag=True, help='show details')
def sum_tool(filename, verbose):
with open(filename) as f:
numbers = [float(line.strip()) for line in f if line.strip()]
total = sum(numbers)
if verbose:
click.echo(f'Found {len(numbers)} numbers. Sum: {total}')
else:
click.echo(total)
if __name__ == '__main__':
sum_tool()
Click automatically generates a nice help page, with the argument type (a path that must exist) and the flag. The click.echo function prints cleanly to the terminal. I like Click when my tool has several subcommands—like a git clone with commit, push, pull. You group commands using @click.group(). Click also supports callbacks, password prompts, and file arguments. It is mature and well‑documented.
One thing I often do with Click is use click.Choice to restrict an option to a set of values:
@click.option('--format', type=click.Choice(['json', 'csv', 'table']), default='json')
That alone prevents a whole class of user errors.
Then came Typer. Typer is built on top of Click, but it uses Python type hints to do most of the work. You write a plain function with type annotations, and Typer turns it into a CLI. No decorators needed for simple cases. It feels like magic the first time you try it.
Look at this:
import typer
def main(filename: str, verbose: bool = False):
with open(filename) as f:
numbers = [float(line.strip()) for line in f if line.strip()]
total = sum(numbers)
if verbose:
typer.echo(f'Found {len(numbers)} numbers. Sum: {total}')
else:
typer.echo(total)
if __name__ == '__main__':
typer.run(main)
Typer reads the function signature. The argument filename: str becomes a positional argument. The keyword argument verbose: bool = False becomes an optional flag --verbose or -v. It even generates a --help message that shows the types and defaults. If you run python sum_tool.py --help, you see something like:
Usage: sum_tool.py [OPTIONS] FILENAME
Sum numbers from a file.
Arguments:
FILENAME [required]
Options:
--verbose / --no-verbose [default: False]
--help Show this message and exit.
I use Typer when I want to turn an existing function into a CLI in ten seconds. It works especially well for small scripts that I build on the fly. If your function has Optional[str], List[int], or custom types, Typer handles those too. You can also use Click decorators inside Typer for advanced cases. Typer produces colored output automatically unless you turn it off.
One trick: if your function returns a string, Typer will print it. And you can use typer.Exit() to stop execution with a specific exit code. Perfect for error handling.
Now, the output. Most tutorials stop after parsing arguments, but real tools need to display results. That’s where Rich comes in. Rich transforms boring terminal output into something you’d expect from a professional application. It gives you tables, progress bars, colored text, markdown display, and even tree structures.
Let me show you a table. I built a tool that queries a database and shows results. Without Rich, I would print rows separated by commas. With Rich, I do this:
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Database Results")
table.add_column("ID", style="cyan", no_wrap=True)
table.add_column("Name", style="magenta")
table.add_column("Score", justify="right", style="green")
table.add_row("1", "Alice", "95")
table.add_row("2", "Bob", "87")
table.add_row("3", "Charlie", "92")
console.print(table)
The output is a neatly aligned table with colored columns. Rich respects your terminal width and wraps text if needed. You can also use Panel, Progress, Layout, and Syntax for highlighting code.
My favourite feature is the progress bar. When your tool downloads files or processes many items, show a progress bar so the user knows something is happening:
import time
from rich.progress import track
for i in track(range(100), description="Processing..."):
time.sleep(0.02)
Rich works inside any CLI tool. I often combine it with Click or Typer. For example, inside a Click command, I create a Console object and use it to print formatted messages. This makes the tool feel polished without much effort.
Sometimes you need more than one‑shot commands. You want an interactive prompt where the user types commands, gets suggestions, and can navigate history. That’s Prompt Toolkit’s domain.
I wrote a little REPL for a custom query language. The user types select * from logs where level=error and gets results. Prompt Toolkit gives me:
- Auto‑completion as the user types.
- Syntax highlighting so keywords look different.
- Multi‑line editing for long queries.
- History that persists between sessions.
Here is a minimal example:
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.history import FileHistory
sql_keywords = ['select', 'from', 'where', 'insert', 'delete']
completer = WordCompleter(sql_keywords)
session = PromptSession(
history=FileHistory('.query_history'),
completer=completer,
complete_while_typing=True
)
while True:
try:
text = session.prompt('sql> ')
if text.strip().lower() == 'exit':
break
# process the query
print(f'You said: {text}')
except KeyboardInterrupt:
continue
except EOFError:
break
When the user types se, the completer suggests select. Pressing Tab completes it. The history is saved to a file, so next time you run the tool, pressing up arrow brings back yesterday’s commands. You can also add custom key bindings, like Ctrl+R for reverse search.
I usually don’t need full REPL capabilities, but when I do, Prompt Toolkit saves me from reinventing the wheel. It is the same library that powers IPython’s terminal interface. If your tool acts as an interactive shell, this is your go‑to library.
Last but not least, Fire. Fire is the opposite of argparse. Instead of writing a parser, you pass any Python object to fire.Fire() and it becomes a CLI. It inspects the object’s attributes and methods and turns them into commands.
For example, I had a class that processes images:
import fire
class ImageProcessor:
def resize(self, filename, width, height):
print(f'Resize {filename} to {width}x{height}')
def convert(self, filename, fmt):
print(f'Convert {filename} to {fmt}')
if __name__ == '__main__':
fire.Fire(ImageProcessor)
Now you can run python tool.py resize image.png 800 600 or python tool.py convert image.png jpg. Fire automatically generates help for each method. It also supports functions, dictionaries, and even modules.
I use Fire for quick prototypes. Suppose I have a script with several functions and I want to expose them all without writing any argument code. Fire does it. It also handles nested objects. If you pass a module, all its functions become subcommands.
The downside is that Fire’s help messages are not as pretty as Click’s, and it can get confused with complex types. But for throw‑away tools or internal utilities, Fire is unbeatable. It turns a class into a CLI in one line.
These six libraries cover every need I have ever had for command‑line tools. Start with argparse if you want the batteries included. Move to Click when you need subcommands and cleaner decorators. Use Typer when you want minimal code and type hints. Add Rich when you want beautiful output. Use Prompt Toolkit for interactive sessions. And grab Fire when you need something working in seconds.
I often combine them. For instance, a Typer command can use Rich inside to print a progress bar. A Click group can have a subcommand that opens an interactive REPL built with Prompt Toolkit. There is no rule that says you must pick only one.
Building a command‑line tool in Python is one of the most rewarding tasks. You create something that lives in the terminal—a constant companion for developers. With these libraries, you can make your tools clear, helpful, and even pleasant to use. Start small. Add one library at a time. You will be surprised how far a little code can go.