Building CLI Applications with Typer

Remember the last time you had to build a command-line tool? If you’re like me, you probably started with argparse or click, wrote boilerplate code, and still ended up with something that felt clunky. That’s where typer comes in – it’s a game-changer that lets you build CLI apps with minimal code. Although there are several other options, typer stands out because it leverages Python’s type hints to do the heavy lifting. No more manual argument parsing! The following snippet shows how to use typer in its simplest form:

import typer

app = typer.Typer()

@app.command()
def hello(name: str):
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    app()

And you will be able to execute it with just:

$ python hello.py Pedro
Hello Pedro!

In this simple example, we were only defining positional arguments, but having optional arguments is as easy as setting default values in the function signature.

import typer

app = typer.Typer()

@app.command()
def hello2(
    name: str,
    count: int = 1,
    favorite_color: str = None
):
    for _ in range(count):
        message = f"Hello {name}!"
        if favorite_color:
            message += f" I like {favorite_color} too!"
        typer.echo(message)

if __name__ == "__main__":
    app()
$ python hello2.py --help

 Usage: hello2.py [OPTIONS] NAME                                                                                                               
                                                                                                                                                   
╭─ Arguments ───────────────────────────────────────────────────────────────────────────────────────────────────────
│ *    name      TEXT  [default: None] [required]                                                                  
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────
╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────
│ --count                     INTEGER  [default: 1]                                                                     
│ --favorite-color            TEXT     [default: None]                                                                  
│ --install-completion                 Install completion for the current shell.                                       
│ --show-completion                    Show completion for the current shell, to copy it or customize the installation.  
│ --help                               Show this message and exit.                                                       
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────



And you can also provide additional metadata, like preferred parameter names or help information by using the Annotated type hint in combination with typer.Argument for positional arguments and typer.Option for optional arguments. And it also allows for some very useful options such as file path validation.

from pathlib import Path
import typer

from typing import Annotated

app = typer.Typer()

@app.command()
def process(
    input_file: Annotated[
        Path,
        typer.Argument(
            exists=True,
            file_okay=True,
            dir_okay=False,
            help="Input file to process"
        )
    ],
    output_dir: Annotated[
        Path,
        typer.Option(
            "--output",
            "-o",
            help="Output directory",
        )
    ] = Path("output"),
    backup: Annotated[
        bool,
        typer.Option(
            help="Create backup before processing"
        )
    ] = False,
):
    """Process a file with progress tracking."""
    
    # Ensure output directory exists
    output_dir.mkdir(parents=True, exist_ok=True)
    
    # Create backup if requested
    if backup:
        backup_file = output_dir / f"{input_file.name}.bak"
        typer.echo(f"Creating backup at {backup_file}")
        backup_file.write_bytes(input_file.read_bytes())
    n = 0
    with open(input_file) as f:
        for line in f:
            n+= len(line)
    with open(output_dir / "count.txt", "w") as f:
        f.write(f"{n}\n")
    
    typer.echo(f"Processing complete! N lines= {n}")
    

if __name__ == "__main__":
    app()

Whether you’re building a simple utility or a complex CLI application, typer’s type-driven approach will save you time and result in better tools. Next time you need to build a CLI tool, give typer a try. Your users (and future self) will thank you!

Author