Typer CLI with subcommands, options and CliRunner tests: working versions (tested 2026-10-15)
Build a Typer CLI with subcommands, typed options, env var fallbacks and exit codes, tested with typer.testing.CliRunner.
passed4 passed · 0 failed2026-10-15 11:57 UTC- Status
- passed
- Last passed
- 2026-10-15 11:57 UTC
- Tests
- 4 / 4
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
typer@0.27.3pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("typer-cli-testing") (install), or see them on GitHub.
requirements.txt
typer==0.27.3 pytest==9.1.1
cli.py
from typing import Annotated
import typer
app = typer.Typer(no_args_is_help=True)
users = typer.Typer(help="Manage users")
app.add_typer(users, name="users")
DB: dict[str, int] = {}
@users.command("add")
def add_user(name: str, age: Annotated[int, typer.Option(min=0, max=150)] = 0):
if name in DB:
typer.echo(f"{name} already exists", err=True)
raise typer.Exit(code=1)
DB[name] = age
typer.echo(f"added {name} ({age})")
@users.command("list")
def list_users():
for n, a in sorted(DB.items()):
typer.echo(f"{n}\t{a}")
@app.command()
def greet(name: Annotated[str, typer.Option(envvar="GREET_NAME")] = "world", shout: bool = False):
msg = f"hello {name}"
typer.echo(msg.upper() if shout else msg)
if __name__ == "__main__":
app()
tests/test_cli.py
from typer.testing import CliRunner
from cli import app, DB
runner = CliRunner()
def setup_function():
DB.clear()
def test_add_and_list():
assert runner.invoke(app, ["users", "add", "ada", "--age", "36"]).exit_code == 0
r = runner.invoke(app, ["users", "list"])
assert "ada\t36" in r.output
def test_duplicate_exits_1():
runner.invoke(app, ["users", "add", "ada"])
assert runner.invoke(app, ["users", "add", "ada"]).exit_code == 1
def test_option_range_validation():
assert runner.invoke(app, ["users", "add", "bo", "--age", "200"]).exit_code == 2
def test_envvar_and_flag():
r = runner.invoke(app, ["greet", "--shout"], env={"GREET_NAME": "jp"})
assert r.output.strip() == "HELLO JP"
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.