Python Typer 完全指南

基於 type hints 的現代 Python CLI 框架,從基礎到進階的完整功能解析與實戰應用


目錄


什麼是 Typer?

TyperSebastián Ramírez(tiangolo) 開發的 Python CLI 框架——他同時也是 FastAPI 的作者。Typer 的設計哲學與 FastAPI 高度一致:用 Python type hints 自動產生功能

import typer

def main(name: str, age: int = 18):
    print(f"Hello {name}, you are {age} years old")

if __name__ == "__main__":
    typer.run(main)

只有這幾行,Typer 就會自動:

  • name: str 推斷出「必填的位置參數」
  • age: int = 18 推斷出「選填的選項,預設 18」
  • 產生 --help 說明文件
  • 自動做型別轉換與驗證
  • 錯誤輸入時印出友善訊息

與其他 CLI 框架的關係

        argparse(標準函式庫)
           ▼  增加裝飾器 + 更多功能
         Click
           ▼  改用 type hints + 簡化 API
         Typer

Typer 建構在 Click 之上——所有 Click 功能都能用,但寫法更現代、更少樣板。


為什麼選 Typer?

與其他 Python CLI 框架對照

框架 風格 學習曲線 適合場景
argparse 標準庫,需手動描述每個參數 中(樣板多) 簡單腳本、不想引入依賴
Click 裝飾器風格,重複定義型別 中大型 CLI、需高自訂性
Typer type hints 自動推斷 現代 Python(≥ 3.8)首選
fire 自動把任意函式變 CLI 極低 快速原型,不在意精準度

Typer 三大優勢

  1. DRY:型別只寫一次(function signature),不必在裝飾器再寫一遍
  2. 編輯器支援:因為用標準 type hints,IDE 自動完成、mypy 檢查全自動可用
  3. Rich 整合:終端輸出色彩、表格、進度條開箱即用

同樣功能的程式碼對比

argparse

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("name", type=str)
parser.add_argument("--age", type=int, default=18)
args = parser.parse_args()
print(f"Hello {args.name}, age {args.age}")

Click

import click

@click.command()
@click.argument("name")
@click.option("--age", default=18, type=int)
def main(name, age):
    click.echo(f"Hello {name}, age {age}")

Typer

import typer

def main(name: str, age: int = 18):
    print(f"Hello {name}, age {age}")

typer.run(main)

★ Insight:Typer 的核心洞見是「Python 3.8+ 已經有 type hints,何必在裝飾器再寫一次?」。函式簽名本身就描述了所有 CLI 行為——必填/選填、型別、預設值。這也是為什麼 Typer 跟 FastAPI 的 API 高度相似:兩者都把「型別系統」當成「設計語言」。


安裝

# 基本安裝(含 Click 依賴)
pip install typer

# 推薦:包含 Rich(彩色終端輸出)
pip install "typer[all]"

# 確認版本
python -c "import typer; print(typer.__version__)"

typer[all] 會額外安裝:

  • rich:彩色文字、表格、進度條
  • shellingham:自動偵測使用者的 shell(產生補全用)

第一個 Typer 程式

單命令(最簡單)

# main.py
import typer

def hello(name: str):
    print(f"Hello {name}")

if __name__ == "__main__":
    typer.run(hello)

執行:

$ python main.py World
Hello World

$ python main.py --help
Usage: main.py [OPTIONS] NAME
Arguments:
  NAME  [required]
Options:
  --help  Show this message and exit.

多命令(使用 Typer 應用程式)

# main.py
import typer

app = typer.Typer()

@app.command()
def hello(name: str):
    print(f"Hello {name}")

@app.command()
def goodbye(name: str):
    print(f"Goodbye {name}")

if __name__ == "__main__":
    app()

執行:

$ python main.py hello World
Hello World

$ python main.py goodbye World
Goodbye World

$ python main.py --help
Commands:
  goodbye
  hello

何時用 typer.run() 何時用 Typer():只有一個函式時用 typer.run();有多個子命令時用 app = typer.Typer() + @app.command()


Arguments vs Options

這是 Typer 最重要的核心觀念——從函式簽名自動分辨

規則

函式簽名寫法 CLI 行為 範例
name: str Argument(位置參數,必填) myapp World
name: str = "default" Option(選項,選填,有預設值) myapp --name World
name: Optional[str] = None Option(選項,可不傳) myappmyapp --name X

實際範例

import typer

def create(
    title: str,                    # Argument:必填位置參數
    body: str = "",                # Option:--body
    public: bool = False,          # Option:--public / --no-public
):
    print(f"Title: {title}")
    print(f"Body: {body}")
    print(f"Public: {public}")

typer.run(create)
$ python main.py "My Post" --body "Hello" --public
Title: My Post
Body: Hello
Public: True

$ python main.py "My Post" --no-public
Title: My Post
Body:
Public: False

顯式控制:typer.Argumenttyper.Option

需要更精細控制時(help 文字、別名、驗證),用 typer.Argument() / typer.Option()

import typer

def create(
    title: str = typer.Argument(..., help="文章標題"),
    body: str = typer.Option("", "--body", "-b", help="文章內容"),
    tags: list[str] = typer.Option([], "--tag", "-t", help="標籤(可多個)"),
):
    ...
  • ... (Ellipsis):表示「沒有預設值,必填」
  • "-b":短選項(-b "Hello" 等同 --body "Hello"
  • list[str]:可以多次傳入,例如 --tag a --tag b

型別系統

Typer 會根據 type hint 自動轉換 + 驗證 CLI 輸入。

基本型別

Python 型別 CLI 行為
str 接受任意字串
int 自動轉 int,非數字會報錯
float 自動轉 float
bool 產生 --flag / --no-flag

進階型別

Python 型別 CLI 行為
pathlib.Path 自動轉 Path 物件,可加 exists=True 檢查
datetime.datetime 自動解析 ISO 8601 字串
Enum 子類 限制只能輸入列舉值(自動 --help 顯示選項)
list[T] / List[T] 同型別可重複,--item a --item b
Optional[T] 可選,預設為 None

Path 範例(檔案存在驗證)

from pathlib import Path
import typer

def process(
    input_file: Path = typer.Argument(
        ...,
        exists=True,        # 檔案必須存在
        file_okay=True,
        dir_okay=False,
        readable=True,
    ),
):
    content = input_file.read_text()
    print(f"Read {len(content)} chars from {input_file}")

typer.run(process)

Enum 範例(限制選項值)

from enum import Enum
import typer

class LogLevel(str, Enum):
    debug = "debug"
    info = "info"
    warning = "warning"
    error = "error"

def run(level: LogLevel = LogLevel.info):
    print(f"Running with log level: {level.value}")

typer.run(run)
$ python main.py --level debug
Running with log level: debug

$ python main.py --level invalid
Error: Invalid value for '--level': 'invalid' is not one of 'debug', 'info', 'warning', 'error'.

小技巧:讓 Enum 繼承 str 是慣例,這樣 level.valuestr(level) 行為一致,序列化也方便。


子命令(Sub-commands)

複雜 CLI 通常有多層命令,例如 git commitgit push。Typer 用「巢狀 Typer 應用」實現。

兩層架構

import typer

app = typer.Typer()
users_app = typer.Typer()
items_app = typer.Typer()

app.add_typer(users_app, name="users")
app.add_typer(items_app, name="items")

@users_app.command("create")
def users_create(name: str):
    print(f"Creating user {name}")

@users_app.command("delete")
def users_delete(id: int):
    print(f"Deleting user {id}")

@items_app.command("list")
def items_list():
    print("Listing items")

if __name__ == "__main__":
    app()

執行:

$ python main.py users create Alice
Creating user Alice

$ python main.py users delete 42
Deleting user 42

$ python main.py items list
Listing items

$ python main.py --help
Commands:
  items
  users

命令結構視覺化

myapp
├── users
│   ├── create
│   └── delete
└── items
    └── list

進階功能

互動式輸入(Prompt)

import typer

def login(
    username: str = typer.Option(..., prompt=True),
    password: str = typer.Option(..., prompt=True, hide_input=True, confirmation_prompt=True),
):
    print(f"Logging in as {username}")

typer.run(login)

執行時,若沒傳參數會互動式詢問:

Username: alice
Password:
Repeat for confirmation:
Logging in as alice

確認動作(Confirmation)

import typer

def delete(
    name: str,
    force: bool = typer.Option(False, "--force", "-f"),
):
    if not force:
        typer.confirm(f"確定要刪除 {name}?", abort=True)
    print(f"Deleted {name}")

typer.run(delete)

Callback(前置處理)

每次執行任何子命令前都會觸發,常用於設定 logging、版本顯示:

import typer

app = typer.Typer()

def version_callback(value: bool):
    if value:
        print("MyApp v1.0.0")
        raise typer.Exit()

@app.callback()
def main(
    verbose: bool = typer.Option(False, "--verbose", "-v"),
    version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True),
):
    if verbose:
        print("Verbose mode on")

@app.command()
def hello(name: str):
    print(f"Hello {name}")

if __name__ == "__main__":
    app()
  • is_eager=True:優先處理(即使有錯也先跑 version)
  • raise typer.Exit():正常結束 CLI

Context(在子命令間共享資料)

import typer

app = typer.Typer()

@app.callback()
def main(ctx: typer.Context, debug: bool = False):
    ctx.ensure_object(dict)
    ctx.obj["debug"] = debug

@app.command()
def run(ctx: typer.Context):
    if ctx.obj["debug"]:
        print("Debug mode")
    print("Running")

if __name__ == "__main__":
    app()

退出碼控制

import typer

def check(value: int):
    if value < 0:
        typer.echo("負數不被允許", err=True)
        raise typer.Exit(code=1)
    print("OK")

typer.run(check)
操作 退出碼
typer.Exit() 0(正常結束)
typer.Exit(code=N) N
typer.Abort() 1(中止,會印 "Aborted!")

Rich 整合

裝了 typer[all] 後,Typer 自動使用 Rich 美化輸出。

彩色文字

import typer

def main(name: str):
    typer.secho(f"Hello {name}", fg=typer.colors.GREEN, bold=True)
    typer.secho("錯誤訊息", fg=typer.colors.RED, err=True)

typer.run(main)

進度條

import time
import typer

def process(items: int = 100):
    with typer.progressbar(range(items), label="處理中") as progress:
        for i in progress:
            time.sleep(0.01)
    print("完成")

typer.run(process)

直接用 Rich 庫

from rich.console import Console
from rich.table import Table
import typer

console = Console()

def list_users():
    table = Table(title="使用者清單")
    table.add_column("ID", style="cyan")
    table.add_column("Name", style="magenta")
    table.add_row("1", "Alice")
    table.add_row("2", "Bob")
    console.print(table)

typer.run(list_users)

測試

Typer 提供 CliRunner(其實是 Click 的)來測試 CLI:

# test_app.py
from typer.testing import CliRunner
from main import app

runner = CliRunner()

def test_hello():
    result = runner.invoke(app, ["hello", "World"])
    assert result.exit_code == 0
    assert "Hello World" in result.stdout

def test_invalid_input():
    result = runner.invoke(app, ["hello"])  # 缺少 name
    assert result.exit_code != 0
    assert "Missing argument" in result.stdout

執行:

pytest test_app.py -v

測試互動式輸入

def test_interactive():
    result = runner.invoke(app, ["login"], input="alice\npass\npass\n")
    assert "Logging in as alice" in result.stdout

打包與部署

用 entry points 安裝為命令

pyproject.toml

[project]
name = "myapp"
version = "0.1.0"
dependencies = ["typer[all]"]

[project.scripts]
myapp = "myapp.main:app"

安裝後即可全域呼叫:

pip install -e .
myapp hello World    # 不需要 python main.py

打包成單檔執行檔

PyInstallershiv

pip install pyinstaller
pyinstaller --onefile main.py
./dist/main hello World    # 不需要 Python 環境

自動產生 shell 補全

# 安裝補全(一次性)
myapp --install-completion

# 重新載入 shell 即可享有 Tab 補全

最佳實踐

1. 用 Annotated(Python 3.9+)寫法更現代

# 舊寫法(仍可用)
def main(name: str = typer.Argument(..., help="使用者名稱")):
    ...

# 新寫法(推薦,Python 3.9+ 含 typing_extensions)
from typing import Annotated

def main(name: Annotated[str, typer.Argument(help="使用者名稱")]):
    ...

新寫法把 Typer 元資料與預設值分離,更符合 PEP 593 標準,IDE 與 mypy 支援更好。

2. 把 CLI 邏輯與商業邏輯分離

# ❌ 把所有邏輯塞進 CLI 函式
def deploy(env: str, force: bool = False):
    # 一堆部署邏輯...
    ...

# ✅ CLI 只負責解析輸入,呼叫純函式
from myapp.deploy import deploy as deploy_logic

def deploy(env: str, force: bool = False):
    deploy_logic(env, force=force)

好處:純函式可獨立測試、可被其他程式碼重用。

3. 善用 Enum 限制選項

# ❌ 字串容易打錯
def deploy(env: str = "prod"):
    if env not in ("dev", "staging", "prod"):
        raise ValueError(...)

# ✅ Enum 自動驗證 + 自動 --help
class Env(str, Enum):
    dev = "dev"
    staging = "staging"
    prod = "prod"

def deploy(env: Env = Env.prod):
    ...

4. 錯誤訊息送 stderr

typer.echo("錯誤訊息", err=True)        # → stderr
typer.secho("錯誤", fg="red", err=True) # → stderr
print("正常輸出")                        # → stdout

讓 shell pipe 行為正確:myapp 2> error.log

5. 重要操作要 confirm

def delete_all():
    typer.confirm("這會刪除所有資料!確定?", abort=True)
    # ...

常見問題

Q1:Typer 跟 Click 該選哪個?

新專案用 Typer——除非你已經有大量 Click 程式碼或需要 Click 特有功能(如複雜的 plugin 系統)。Typer 寫法更精簡、編輯器支援更好,且底層就是 Click,未來想轉換成本低。

Q2:可以混用 Typer 和 argparse 嗎?

不建議。兩者解析模型不同,混用會讓 --help 不一致。如果只是想保留舊腳本,建議分階段重寫成 Typer。

Q3:怎麼處理「不固定數量的位置參數」?

list[str]

def deploy(envs: list[str]):
    for env in envs:
        print(f"Deploying {env}")
$ python main.py dev staging prod
Deploying dev
Deploying staging
Deploying prod

Q4:怎麼讀取環境變數?

envvar 參數:

def main(api_key: str = typer.Option(..., envvar="MY_API_KEY")):
    ...
$ MY_API_KEY=xxx python main.py
$ python main.py --api-key xxx    # 兩種都行

Q5:怎麼讓 Typer 不在錯誤時顯示完整 traceback?

預設行為已經是「發生 typer.Exit()typer.Abort() 時不顯示 traceback」。若你想連 Python exception 也壓下:

app = typer.Typer(pretty_exceptions_enable=False)

Q6:如何讓某些命令不出現在 --help

@app.command(hidden=True)
def debug_internal():
    ...

或單一選項:

debug: bool = typer.Option(False, "--debug", hidden=True)

總結

核心要點

  1. type hints 自動推斷:函式簽名 = CLI 介面定義
  2. Argument vs Option:「沒有預設值 = Argument」「有預設值 = Option」
  3. Click 的繼承:所有 Click 功能可用,但寫法更現代
  4. Rich 整合:彩色、表格、進度條開箱即用
  5. 編輯器友善:mypy / IDE 自動完成全部可用

速查表

需求 寫法
必填位置參數 name: str
選項(含預設值) name: str = "default"
短選項別名 typer.Option(..., "-n")
多次傳入 tags: list[str]
限制可選值 Enum 子類
檔案路徑驗證 Path = typer.Argument(..., exists=True)
互動式輸入 typer.Option(..., prompt=True)
隱藏輸入 prompt=True, hide_input=True
退出 raise typer.Exit(code=N)
中止 raise typer.Abort()
確認對話 typer.confirm(..., abort=True)
環境變數 typer.Option(..., envvar="VAR")

學習路徑建議

1. typer.run(func)         ← 5 分鐘上手
2. @app.command()          ← 多命令
3. typer.Argument/Option   ← 自訂 help、別名
4. Enum + Path             ← 型別驗證
5. Sub-typer 巢狀          ← 大型 CLI
6. Callback + Context      ← 全域選項與狀態共享
7. Annotated 寫法 + 測試   ← 進階品質

延伸閱讀


建立日期:2026-04-30 最後更新:2026-04-30

🔗相關文章