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!
