Typer: A Modern CLI Framework for Building Command-Line Tools with Type Hints and Autocompletion in Python

Do you remember the pain of writing command-line tools with argparse for the first time? Those numerous add_argument calls, having to check the documentation every time to know how to set parameter types.

The most frustrating part is when users use the wrong parameters, and the error messages are particularly hard to understand. I thought to myself, can’t this be simpler?

01

By chance, I discovered the Typer framework. Honestly, I was shocked when I first saw its code example.

import typer

def main(name: str, age: int = 20):
    """A simple greeting program"""
    typer.echo(f"Hello {name}, you are {age} years old!")

if __name__ == "__main__":
    typer.run(main)

With just a few lines of code, it can generate a complete command-line tool? Save it as hello.py and run:

python hello.py John --age 25
# Output: Hello John, you are 25 years old!

It automatically helps you parse parameter types and handle default values.

I thought this was simply magic.

02

After diving deeper, I found that the core idea of Typer is to utilize Python’s type hints. When you write functions, you need to annotate parameter types anyway, and Typer directly uses this information to generate the CLI interface.

from typing import Optional
import typer

def process_file(
    input_file: str,
    output_file: Optional[str] = None,
    verbose: bool = False,
    count: int = 1
):
    """Command-line tool for processing files"""
    if verbose:
        typer.echo(f"Processing {input_file}...")

    # Your business logic
    for i in range(count):
        typer.echo(f"Round {i+1}")

    if output_file:
        typer.echo(f"Results saved to {output_file}")

if __name__ == "__main__":
    typer.run(process_file)

This allows users to call it like this:

python tool.py input.txt --output-file result.txt --verbose --count 3

Parameter names are automatically converted to kebab-case, and type checking is done automatically.

Once you use it, you won’t want to go back.

03

What truly made me fall in love with Typer is its autocompletion feature. Previously, when writing tools with argparse, users had to remember all parameter names, which was super cumbersome.

Typer can generate shell completion scripts with one command:

import typer

app = typer.Typer()

@app.command()
def create(name: str, type: str = "default"):
    """Create a new project"""
    typer.echo(f"Creating {type} project: {name}")

@app.command()
def delete(name: str, force: bool = False):
    """Delete a project"""
    if force:
        typer.echo(f"Force deleting: {name}")
    else:
        typer.echo(f"Deleting: {name}")

if __name__ == "__main__":
    app()

Then generate the completion script:

python tool.py --install-completion

After that, you can use the Tab key to autocomplete commands and parameters.

This feature is particularly useful when promoting tools within a team, making it much easier for everyone to use.

04

What impressed me the most was Typer’s ability to handle complex scenarios. For example, when options need to be selected:

from enum import Enum
import typer

class Color(str, Enum):
    red = "red"
    green = "green"
    blue = "blue"

def paint(color: Color, intensity: float = 1.0):
    """Color something"""
    if intensity > 1.0:
        typer.echo("Intensity too high!", err=True)
        raise typer.Exit(1)

    typer.echo(f"Painting with {color.value} at {intensity} intensity")

if __name__ == "__main__":
    typer.run(paint)

When users input an incorrect color value, it automatically suggests the available options. This user experience is far superior to writing validation logic yourself.

There’s also file path validation:

from pathlib import Path
import typer

def backup(source: Path, destination: Path):
    """Backup files or directories"""
    if not source.exists():
        typer.echo(f"Source {source} doesn't exist!", err=True)
        raise typer.Exit(1)

    typer.echo(f"Backing up {source} to {destination}")

if __name__ == "__main__":
    typer.run(backup)

The Path type automatically handles path parsing and can also perform existence checks.

05

After extensively using Typer in projects, I summarized a few best practices.

First, error handling should be elegant:

import typer

def risky_operation(file_path: str):
    """An operation that may fail"""
    try:
        # Your business logic
        process_file(file_path)
        typer.echo("Success!", fg=typer.colors.GREEN)
    except Exception as e:
        typer.echo(f"Error: {e}", err=True, fg=typer.colors.RED)
        raise typer.Exit(1)

Progress bars are also very useful:

import time
import typer

def long_task(items: int = 100):
    """A time-consuming task"""
    with typer.progressbar(range(items)) as progress:
        for item in progress:
            time.sleep(0.1)  # Simulate work

    typer.echo("Done!")

These small details make the tool feel more professional.

Now, almost all CLI tools in the team have been rewritten using Typer, significantly improving development efficiency and user experience. If you are still using argparse to write command-line tools, I really recommend trying Typer.

Type hints are something you need to write anyway, so why not let them play a greater role?

Leave a Comment