Use Typer for CLI Tools

Use Typer for CLI tools — Applied AI engineering.

Focus: use typer for cli tools

Sponsored

Your AI models are impressive, but the moment you need to share them with a teammate or deploy them to production, you hit an awkward wall: how does anyone actually run your inference script? Passing arguments through sys.argv with manual parsing is brittle. Click is verbose. argparse feels antiquated. The pain is real — a CLI tool that's hard to use or maintain undermines even the best LLM pipeline. The solution is Typer, the modern CLI library that turns your Python functions into polished command-line interfaces with almost zero boilerplate. In this lesson, you'll learn why Typer is the go-to choice for Applied AI engineers and how to wield it effectively.

The problem this lesson solves

You've written a brilliant script that calls an LLM API, does some retrieval, and returns a structured JSON. But how do you make it usable by a non-developer? Or by a CI pipeline? You need a command-line interface that:

  • Lets users pass parameters like --model, --temperature, or --input-file
  • Provides clear help messages and error feedback
  • Is robust enough for production use
  • Is fast to build and easy to extend

Using raw sys.argv leads to spaghetti code and bugs. argparse is verbose and its API feels dated. Click is powerful but requires a learning curve. Typer, built on top of Click, combines modern type hints with a simple, intuitive API. In an AI engineering context, where you're constantly prototyping new data-prep scripts, evaluation harnesses, or model-deployment utilities, Typer lets you ship a usable CLI in minutes, not hours.

Core concept / mental model

Typer uses Python's type hints to infer CLI argument types, defaults, and help text. Instead of explicitly defining each argument in a separate parser, you write ordinary Python functions with annotated parameters. Typer introspects the function signature and builds a CLI around it.

Think of your function as the 'brain' and Typer as the 'interpreter' — it translates command-line strings into Python types, handles validation, and generates help text automatically. This is a declarative approach: you declare what you want, and Typer handles the plumbing.

A quick mental model: Typer is to Click what FastAPI is to Flask. FastAPI uses type hints to create APIs; Typer uses the same philosophy for CLIs. If you've used FastAPI, Typer will feel instantly familiar.

Here's a high-level view of how it works:

  • The typer.Typer() object creates a new CLI application.
  • Decorators like @app.command() register functions as commands.
  • Function parameters become CLI arguments or options, depending on how you use defaults and annotations.
  • Running the script without arguments displays a help message, because Typer automatically adds --help.

How it works step by step

Let's break down the core mechanics:

  1. Import and instantiate: Create a Typer instance. This is the core object that holds your commands.
  2. Define a command function: Write a normal Python function. Use type hints for each parameter.
  3. Decorate with @app.command(): This tells Typer to expose the function as a subcommand.
  4. Use the function: You can pass arguments, options, flags, and even read from standard input. Typer converts the string input to the declared type.
  5. Run the app: The if __name__ == "__main__": app() pattern starts the CLI.

Key concepts to understand:

  • Arguments are positional parameters with no default value.
  • Options are named parameters, often with defaults. Use --name style by giving a default value.
  • Flags are boolean options that can be enabled with --flag.
  • Type hintsstr, int, float, bool, Path, List[str], etc. — determine how Typer parses input.
  • Default values — if a parameter has a default, it becomes an option; otherwise it's an argument.
  • typer.Option() and typer.Argument() let you control help text, prompt, and more.

Hands-on walkthrough

Let's build a practical CLI tool: an AI inference runner that calls an LLM API and prints the response. We'll cover basic commands, options, and a subcommand structure.

Step 1: Install Typer

pip install "typer[all]"

The [all] extra installs rich for pretty output, but it's optional.

Step 2: Your first Typer CLI

Create a file hello.py:

import typer

app = typer.Typer()

@app.command()
def greet(name: str, greeting: str = "Hello"):
    """Greet someone with a custom greeting."""
    typer.echo(f"{greeting}, {name}!")

if __name__ == "__main__":
    app()

Run it:

python hello.py World
# Output: Hello, World!

python hello.py --greeting Hi World
# Output: Hi, World!

python hello.py --help
# Shows usage, arguments, and options

Step 3: An AI inference CLI

Now let's build something more relevant to AI — a script that simulates calling an LLM and returning a response based on the model and temperature.

# inference_cli.py
import typer
from typing import Optional

app = typer.Typer(help="Run AI model inference from the command line.")

@app.command()
def run(
    prompt: str = typer.Argument(..., help="Prompt to send to the model"),
    model: str = typer.Option("gpt-3.5-turbo", help="Model name to use"),
    temperature: float = typer.Option(0.7, min=0.0, max=2.0, help="Sampling temperature"),
    verbose: bool = typer.Option(False, help="Print extra info")
):
    """Run a single inference pass with the given prompt."""
    if verbose:
        typer.echo(f"Using model: {model}")
        typer.echo(f"Temperature: {temperature}")
    # Simulate an API call
    result = f"Simulated response for: {prompt}"
    typer.echo(result)

if __name__ == "__main__":
    app()

Test it:

python inference_cli.py "Hello, how are you?" --verbose
# Output:
# Using model: gpt-3.5-turbo
# Temperature: 0.7
# Simulated response for: Hello, how are you?

Step 4: Multiple commands and subapps

In real projects, you often have several related commands. You can group them under one app.

# ai_utils.py
import typer
from enum import Enum

class Format(str, Enum):
    json = "json"
    text = "text"

app = typer.Typer()

@app.command()
def preprocess(in_file: typer.FileText, out_file: typer.FileText, lower: bool = True):
    """Preprocess a text file."""
    for line in in_file:
        processed = line.lower() if lower else line
        out_file.write(processed)
    typer.echo("Preprocessing complete.")

@app.command()
def infer(prompt: str, fmt: Format = Format.text, max_tokens: int = 100):
    """Run inference and output in a specified format."""
    result = f"Response to '{prompt}'"
    if fmt == Format.json:
        typer.echo(f'{{"response": "{result}", "max_tokens": {max_tokens}}}')
    else:
        typer.echo(result)

if __name__ == "__main__":
    app()

Now python ai_utils.py --help shows both commands, and each has its own help.

Step 5: Using environment variables and config

In production, you often need to read secrets like API keys. Typer integrates with os.environ for defaults.

import typer
import os

app = typer.Typer()

@app.command()
def query(prompt: str, api_key: str = typer.Option(..., envvar="OPENAI_API_KEY", help="API key")):
    """Query an LLM API using an API key from env."""
    typer.echo(f"Using API key: {api_key[:5]}...")
    typer.echo(f"Prompt: {prompt}")

if __name__ == "__main__":
    app()

Run with export OPENAI_API_KEY=sk-... and then python query.py "Hi".

Compare options / when to choose what

Not every CLI library fits every need. Here's a decision guide:

Library Pros Cons Best for
Typer Type-hint friendly, minimal boilerplate, auto help, built on Click Requires Python 3.6+, can be too high-level for exotic needs Modern AI/DevOps tools where developer experience matters
Click Stable, huge ecosystem, fine-grained control Verbose, more boilerplate Large existing Click-based projects
argparse Standard library, no dependencies Regex-like parsing, manual help messages, clunky Small scripts, where external deps aren't allowed
Fire Auto-generates CLI from any Python object Less control over argument types, error messages can be vague Quick and dirty exploration

Variation 1: Use Typer with Rich to add syntax-highlighted output and progress bars for long-running tasks. Variation 2: Use Pydantic models inside your Typer commands for validation and structured data — a natural fit if you already use Pydantic in AI projects. Variation 3: Use Typer's callback feature to run setup/teardown logic, like loading a model once for multiple commands.

When to choose what:

  • For AI/ML pipelines) — Typer is your friend. It's fast to write, clear to read, and easy to maintain.
  • For audit-tough security tools) — Click or argparse might give you finer control, but Typer is still solid.
  • For scripts that run rarely) — argparse is fine if you want zero dependencies.
  • For interactive REPLs) — Typer isn't designed for that; look at cmd or prompt_toolkit.

Troubleshooting & edge cases

Even with Typer, you'll hit pitfalls. Here are common ones with fixes.

Problem: "Command not found" when running python script.py without arguments

  • Cause: You forgot if __name__ == "__main__": and the app() call.
  • Fix: Always end with that block. If you use Typer as a console script entry point, you define it in setup.py/pyproject, but for a script, the __main__ block is essential.

Problem: Boolean options don't work as expected

  • Declaring debug: bool = False makes --debug an option flag. But if you use debug: bool without a default, Typer will treat it as argument that expects True/False — confusing.
  • Fix: Always give a boolean a default value: debug: bool = typer.Option(False, "--debug").

Problem: typer.Argument(...) vs default values

  • If you use typer.Argument(...) for required positional, you must pass it at the command line. If you give a default, it becomes optional.
  • Fix: Use typer.Argument(None, help="...") for optional positional.

Problem: Reading typer.FileText gives "File not found"

  • Typer's typer.FileText opens the file for reading, but it expects a path that exists. If you want to create a file, use typer.FileTextWrite or typer.FileBinaryWrite.

Problem: Errors are too verbose or not verbose enough

  • Typer uses Click under the hood, so you can customise error handling with @app.result_callback() or by catching exceptions and using typer.echo.
  • For a global error handler, wrap the function body in try/except and use raise typer.Exit(code=1).

Problem: Enabling autocompletion

  • Typer supports shell completions for bash, zsh, fish. Generate the completion script with python script.py --install-completion (if you use click-completion), but note that it's optional and requires external install. The simplest: no extra setup for basic use.

Problem: Passing a list of strings

  • Use List[str] from typing as a parameter type. Typer will accept multiple values separated by spaces.
from typing import List

def process(items: List[str]): ...

What you learned & what's next

In this lesson, you've learned how to use Typer for CLI tools — from the basic typer.Typer() app to advanced features like enums, file handling, and environment variables. You can now:

  • Explain why Typer is the modern choice for CLI development in AI engineering.
  • Create a functional CLI tool with arguments, options, and multiple commands.
  • Apply type hints to enforce correct input and generate excellent help messages.
  • Compare Typer with Click and argparse to make an informed choice for your project.
  • Troubleshoot common pitfalls and edge cases.

Your next step in the Applied AI engineering path is to integrate Typer into a real AI utility — perhaps a data-prep script or a model evaluation harness. The skills you've built here are foundational to building deployable AI tools.

Now go build a CLI for your AI project — it's an investment that pays off every single time you or your teammates run a command.

Practice recap

Build a small CLI that reads a CSV file of product names, calls a mock LLM to generate a marketing description for each, and writes the results to a new CSV. Use Typer with a --model option and an --output argument. Then run --help to admire the automatic documentation.

Common mistakes

  • Forgetting the if __name__ == "__main__": app() block — the script runs silently without it when invoked directly.
  • Declaring a boolean parameter without a default, causing Typer to treat it as a positional argument expecting True/False.
  • Using typer.FileText for a file that needs to be created — it only opens existing files; use typer.FileTextWrite instead.
  • Mixing typer.Argument(...) with a default value, which makes it optional but still shows in help as an argument — inconsistent behavior.
  • Not using envvar for API keys, hardcoding secrets directly in the CLI code, which is a security risk.

Variations

  1. Use Typer with Rich to display colored output and progress bars for long-running model inference or data pipelines.
  2. Integrate Pydantic models inside Typer commands for complex input validation and structured configuration files.
  3. Leverage Typer's callback feature to load a model once and share it across multiple subcommands, improving performance.

Real-world use cases

  • A CLI tool for data scientists to run model inference on a dataset, specifying model name, batch size, and output format.
  • A DevOps script to trigger a training job on a cloud platform, with options for hyperparameters, dataset path, and environment variables.
  • An internal utility to preprocess and split large text corpora for fine-tuning, with commands for cleaning, tokenization, and sampling.

Key takeaways

  • Typer uses type hints to automatically build CLIs — faster and less boilerplate than argparse or Click.
  • Function parameters without defaults become positional arguments; those with defaults become named options.
  • Typer automatically generates --help pages, making your tools self-documenting.
  • Use envvar to read configuration from environment variables, keeping secrets out of code.
  • Boolean options must always have a default value to work as flags.
  • Group related commands under one Typer app for a coherent, extensible CLI suite.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.