Use Typer for CLI Tools
Use Typer for CLI tools — Applied AI engineering.
Focus: use typer for cli tools
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:
- Import and instantiate: Create a
Typerinstance. This is the core object that holds your commands. - Define a command function: Write a normal Python function. Use type hints for each parameter.
- Decorate with
@app.command(): This tells Typer to expose the function as a subcommand. - Use the function: You can pass arguments, options, flags, and even read from standard input. Typer converts the string input to the declared type.
- 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
--namestyle by giving a default value. - Flags are boolean options that can be enabled with
--flag. - Type hints —
str,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()andtyper.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
cmdorprompt_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 theapp()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 = Falsemakes--debugan option flag. But if you usedebug: boolwithout a default, Typer will treat it as argument that expectsTrue/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.FileTextopens the file for reading, but it expects a path that exists. If you want to create a file, usetyper.FileTextWriteortyper.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 usingtyper.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]fromtypingas 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.FileTextfor a file that needs to be created — it only opens existing files; usetyper.FileTextWriteinstead. - Mixing
typer.Argument(...)with a default value, which makes it optional but still shows in help as an argument — inconsistent behavior. - Not using
envvarfor API keys, hardcoding secrets directly in the CLI code, which is a security risk.
Variations
- Use Typer with Rich to display colored output and progress bars for long-running model inference or data pipelines.
- Integrate Pydantic models inside Typer commands for complex input validation and structured configuration files.
- 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
--helppages, making your tools self-documenting. - Use
envvarto 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.