Python CLI Tool: How to Build Your First Typer Command
A Python CLI tool is a command-line application your users run from the terminal with a command like mytool hello John. Typer makes building these tools intuitive by using Python type hints and modern async patterns. In this article, you'll write your first working CLI application in under 50 lines of code.
What is Typer and Why Use It?
Typer is a modern Python framework for building CLIs that runs on top of Click and features optional type hints for validation and help generation. According to the Typer documentation (2026), it reduces boilerplate by 60% compared to argparse and provides automatic help text, type checking, and error messages out of the box. Unlike older approaches, Typer integrates with FastAPI-style async/await patterns, making it feel native to modern Python code.
Installing Typer
Before writing your first command, install Typer using pip:
pip install typer
This installs both Typer and its core dependency, Click. No additional configuration is needed. The package is actively maintained and compatible with Python 3.7+.
Your First Typer Command
Here's a complete, runnable CLI application:
import typer
app = typer.Typer()
@app.command()
def hello(name: str):
"""Greet someone by name."""
typer.echo(f"Hello, {name}!")
if __name__ == "__main__":
app()
Save this as hello.py and run it:
python hello.py --help
python hello.py "Alice"
The output will be Hello, Alice!. Let's break down what each line does:
import typer— imports the Typer framework.app = typer.Typer()— creates a CLI application instance.@app.command()— decorator that registers thehellofunction as a CLI command.name: str— type hint that makes Typer enforce thatnamemust be provided and is a string.typer.echo()— Typer's wrapper aroundprint()for better Unicode and color support.
Adding a Default Value
Commands often have optional parameters with defaults. Here's the pattern:
import typer
app = typer.Typer()
@app.command()
def greet(name: str = "World", count: int = 1):
"""Greet someone multiple times."""
for _ in range(count):
typer.echo(f"Hello, {name}!")
if __name__ == "__main__":
app()
Run it:
python hello.py greet
python hello.py greet --name Alice --count 3
python hello.py greet Alice 3
When you don't provide --name or --count, Typer uses the defaults. Type hints like int automatically convert the string input "3" to the integer 3.
Understanding the Help System
Typer auto-generates help from your function's docstring and type hints:
python hello.py greet --help
Output:
Usage: hello.py greet [OPTIONS]
Greet someone multiple times.
Options:
--name TEXT [default: World]
--count INTEGER [default: 1]
--help Show this message and exit.
This help is generated automatically without any extra configuration. The docstring becomes the command description, and parameter defaults appear in the help text.
Handling Exit Codes
Professional CLIs return proper exit codes to signal success or failure:
import typer
app = typer.Typer()
@app.command()
def validate_email(email: str):
"""Check if an email looks valid."""
if "@" in email and "." in email:
typer.echo(f"Valid: {email}")
else:
typer.echo("Invalid email format.", err=True)
raise typer.Exit(code=1)
if __name__ == "__main__":
app()
When the email is invalid, the command exits with code 1 (standard error code), which scripts and CI/CD systems recognize. The err=True parameter on typer.echo() prints to stderr instead of stdout.
Key Takeaways
- Typer apps use
@app.command()decorators and type hints to define CLI commands. - Function parameters become command-line arguments; type hints handle validation and conversion.
- Docstrings automatically populate help text via
--help. - Use
typer.echo()for Unicode-safe output andtyper.Exit()to set exit codes. - Default parameter values become optional CLI flags with smart help generation.
Frequently Asked Questions
How do I run my Typer app directly without saying python?
Make your file executable with chmod +x hello.py and add a shebang line #!/usr/bin/env python3 at the top. On Windows, packaging the tool as a PyPI package (covered in article 9) is the standard approach.
Can Typer handle boolean flags like --verbose?
Yes. Use bool type hints with a default: verbose: bool = False. Typer converts --verbose or --verbose true to boolean automatically. For flag-style arguments, use typer.Option() with is_flag=True.
What's the difference between positional and named arguments?
Positional arguments like python hello.py Alice are passed in order. Named arguments use --name Alice. Typer treats function parameters as named by default; to make them positional, you use special syntax covered in article 2.
Does Typer work with async functions?
Yes. Define your command as async def and Typer runs it with asyncio.run() automatically. This is useful for I/O-bound operations like API calls or file I/O.
How do I add a version flag like --version?
Pass --version as a callback during app creation: app = typer.Typer(invoke_without_command=False, pretty_exceptions_enable=False, no_args_is_help=True) plus a version callback function, or use typer-cli for a simpler approach (covered in packaging articles).