A modern CLI framework built on Click that derives commands, options, and validation from standard Python type hints — the least-boilerplate way to build robust command-line tools.
Typer (from the FastAPI author) uses type annotations to define a CLI: a function's parameters become arguments and options, their types drive parsing and validation, and help text comes from docstrings. It inherits Click's power (subcommands, prompts, completion) while cutting the decorator noise. For security tooling it means you can turn a typed function into a documented, validated CLI in a few lines.
pip install "typer[all]"Note
The [all] extra pulls in Rich for colorized help and tracebacks.
from typing import Optional
import typer
app = typer.Typer(help="Scan hosts you are authorized to test.")
@app.command()
def scan(
target: str,
ports: str = "1-1024",
workers: int = 50,
output: Optional[str] = None,
verbose: bool = False,
):
"""Scan TARGET for open ports."""
if verbose:
typer.echo(f"workers={workers}")
typer.echo(f"scanning {target} ports {ports}")
if output:
typer.echo(f"writing to {output}")
if __name__ == "__main__":
app()Typer derives the CLI from type hints: workers: int becomes --workers INTEGER with validation, and verbose: bool becomes a flag. It is built on Click, so Click concepts carry over.
| API | Purpose |
|---|---|
typer.Typer() |
The application object |
@app.command() |
Register a command |
app.add_typer(sub, name=) |
Compose sub-applications |
typer.Argument(default, help=) |
Configure a positional argument |
typer.Option(default, "--name", "-n", help=) |
Configure an option |
typer.Option(..., prompt=True, hide_input=True) |
Prompt for a secret |
typer.Option(..., envvar="NAME") |
Read from the environment |
typer.echo(msg) / typer.secho(msg, fg=) |
Output, optionally coloured |
typer.confirm(text) |
Yes/no prompt |
typer.progressbar(iterable) |
Progress display |
typer.Exit(code=1) |
Exit with a status |
typer.BadParameter(msg) |
Raise a usage error |
CliRunner() (from Click) |
Test harness |
Annotating a parameter as Path or an Enum gives you path and choice validation automatically.
A typed scanner command:
import typer
app = typer.Typer(help="Authorized recon toolkit.")
@app.command()
def scan(target: str, ports: str = "1-1024", verbose: bool = False):
"""Port-scan an authorized TARGET."""
typer.echo(f"Scanning {target} ports {ports}")
if verbose:
typer.echo("Verbose mode on")
if __name__ == "__main__":
app()$ python scan.py 192.168.56.10 --ports 1-100 --verbose
Scanning 192.168.56.10 ports 1-100
Verbose mode on
Multiple subcommands and enum-validated choices:
from enum import Enum
import typer
app = typer.Typer()
class Proto(str, Enum):
tcp = "tcp"
udp = "udp"
@app.command()
def sweep(subnet: str, proto: Proto = Proto.tcp):
"""Sweep a SUBNET using PROTO (tcp/udp)."""
typer.echo(f"[sweep] {subnet} over {proto.value}")
@app.command()
def dns(domain: str):
"""Enumerate DNS records for DOMAIN."""
typer.echo(f"[dns] {domain}")
if __name__ == "__main__":
app()$ python recon.py sweep 192.168.56.0/24 --proto udp
[sweep] 192.168.56.0/24 over udp
Secure password prompt and confirmation:
import typer
def main(user: str, password: str = typer.Option(..., prompt=True, hide_input=True)):
"""Authenticate USER, prompting for a hidden password."""
typer.echo(f"Authenticating {user} ({len(password)} char secret)")
if __name__ == "__main__":
typer.run(main)$ python login.py tester
Password:
Authenticating tester (9 char secret)
- Rapid tool scaffolding — expose a typed function as a validated CLI in minutes for one-off engagement scripts.
- Self-documenting toolkits — type hints + docstrings generate
--helpand enforce argument types automatically. - Choice / range validation — use
Enumand typed params to constrain protocols, modes, and ranges before execution. - Safe secret entry — hidden prompts keep credentials out of shell history and process listings.
- Shell completion — ship tab-completion for your recon tool to speed operator workflows.
- Omitting type hints — Typer derives everything from them; without hints you lose validation entirely.
- Using
Optional[str]without a default, making an option unexpectedly required. - Forgetting
app()in the__main__block. - Expecting
boolto take a value — it becomes a--flag/--no-flagpair, not--flag=true. - Using
typer.Option(...)with a literal Ellipsis without realising it marks the option required. - Accepting secrets as plain options instead of
prompt=True, hide_input=True. - Assuming Typer replaces Click — it wraps it, so Click's documentation still applies for advanced cases.
[!warning] Authorized use only Put the scope reminder in the app or command help text, and enforce authorization in the engine rather than trusting the CLI.
- Type hints are validation.
port: intand anEnumfor output format reject bad input before your code runs — use them rather than casting by hand. - Never accept credentials as a plain option. Use
prompt=True, hide_input=Trueorenvvar=; command-line arguments are world-readable viaps. - Path parameters still need containment checks for untrusted input; annotation validates existence, not safety.
- Do not interpolate parsed values into shell strings — pass an argument list to
subprocess.run(). - Provide
--dry-runand confirmation for anything that generates significant traffic or makes changes. - Return meaningful exit codes with
typer.Exit(code=...)so the tool can gate automation.
- Annotate every parameter; the types are your validation layer.
- Use
Enumsubclasses for fixed choices instead of free-form strings. - Prompt for secrets with
typer.Option(..., prompt=True, hide_input=True). - Split large tools into
@app.command()subcommands for clarity. - Install
typer[all]so users get Rich-formatted help and errors.
- [[Click]] — the framework Typer is built on
- [[Rich]] — powers Typer's colorized help and output
- [[Readme|Python for Security Professionals]] — course home