一、前言 #
嗨,我是拍拍君 🐍
很多 Python 腳本剛開始都很單純,先寫一支 python job.py 能跑就好。可是只要腳本開始被重複使用,需求很快就會長出來,像 --input、--output、--verbose、--dry-run、--limit,甚至後來還會想拆成 fetch、report、export 這些子命令。這時如果你還在用 sys.argv 手工切字串,通常很快就會進入混亂模式。
argparse 是 Python 標準庫提供的 CLI 參數解析工具。它的特色不是花俏,而是穩、夠用、零依賴。你可以用它處理型別轉換、預設值、必填規則、help 訊息、子命令分派,而且幾乎所有 Python 環境都有它。
這篇文章會從最小範例一路帶到比較實戰的設計。讀完你會知道:
- positional arguments 和 optional arguments 怎麼分
type、default、choices、nargs怎麼用store_true和count這些旗標設計為什麼實用subparsers怎麼做出像樣的多命令工具- help 訊息和 parser 結構怎麼寫比較耐維護
如果你之後想往更有框架感的方向走,也可以延伸讀 Python Click 教學:打造漂亮 CLI 工具 和 Python Typer 入門:用型別註解打造現代 CLI。但先把 argparse 練熟,真的很值得。
二、安裝 #
2.1 argparse 是標準庫
#
先講一個很實際的優點,argparse 不用安裝。只要你有 Python 3,就可以直接用。
python --version
# Python 3.11.9
import argparse
如果你是用 uv 管環境,也一樣不需要額外安裝套件:
uv init demo-argparse
cd demo-argparse
uv run python -c "import argparse; print('ok')"
這讓 argparse 特別適合內部工具、研究流程腳本、部署助手、資料清理工具,因為你不用擔心對方環境少裝某個第三方 CLI framework。
2.2 第一個 parser #
先看一個最小可用範例:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("name")
args = parser.parse_args()
print(f"Hello, {args.name}!")
執行:
python hello.py 拍拍君
# Hello, 拍拍君!
如果忘了給參數:
python hello.py
你會看到:
usage: hello.py [-h] name
hello.py: error: the following arguments are required: name
這裡已經能看出 argparse 的價值。你不用自己手刻 usage,也不用再寫一堆「如果長度不夠就印錯誤」的樣板。
2.3 兩種核心參數 #
先記住一件事,CLI 的參數通常分成兩類:
- positional arguments,靠位置判斷,例如
name - optional arguments,靠旗標判斷,例如
--verbose
這個分類很重要。因為一支工具該不該用 positional,常常影響使用體驗。必要且順序自然的值適合 positional。比較像設定開關、輸出格式、限制數量的,通常適合 optional。
三、基本參數設計 #
3.1 positional arguments 與型別驗證 #
先來做一個最簡單的加法器:
import argparse
parser = argparse.ArgumentParser(description="兩個整數相加")
parser.add_argument("x", type=int, help="第一個整數")
parser.add_argument("y", type=int, help="第二個整數")
args = parser.parse_args()
print(args.x + args.y)
正常執行:
python add.py 3 5
# 8
錯誤輸入:
python add.py 3 apple
usage: add.py [-h] x y
add.py: error: argument y: invalid int value: 'apple'
這裡的 type=int 很值得養成習慣。它不只轉型,也順便把錯誤訊息統一處理掉。CLI 在入口就做驗證,後面會省很多麻煩。
3.2 optional arguments、預設值與合法值 #
接著加一個可選的輸出風格:
import argparse
parser = argparse.ArgumentParser(description="兩個整數相加")
parser.add_argument("x", type=int)
parser.add_argument("y", type=int)
parser.add_argument("--style", default="plain", choices=["plain", "pretty"])
args = parser.parse_args()
result = args.x + args.y
print(f"結果是:{args.x} + {args.y} = {result}" if args.style == "pretty" else result)
python add.py 3 5
python add.py 3 5 --style pretty
default 和 choices 幾乎是 argparse 最常用的兩個設計。前者讓工具在沒給參數時也能合理工作,後者則把規則直接鎖在 parser 裡,不必等執行到後面才發現值不合法。
3.3 短旗標與長旗標 #
高頻參數通常會同時提供短版和長版:
parser.add_argument("-o", "--output", default="result.txt", help="輸出檔名")
這樣使用者可以自由選擇:
python tool.py input.txt -o out.txt
python tool.py input.txt --output out.txt
短旗標適合日常高頻操作,長旗標適合可讀性。拍手君自己的偏好是,只要某個參數很常打,就值得給短旗標。
3.4 布林旗標:store_true
#
很多 CLI 會出現這些開關型參數:--verbose、--dry-run、--force。這種情況最常見的寫法是 store_true:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("source")
parser.add_argument("target")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
if args.dry_run:
print(f"[DRY RUN] would copy {args.source} -> {args.target}")
else:
print(f"copy {args.source} -> {args.target}")
這種設計很直覺,有給旗標就是 True,沒給就是 False。對會影響副作用的操作,像刪檔、同步、部署,--dry-run 幾乎是必備好習慣。
3.5 count 和 nargs
#
有時候你不只想知道有沒有開 verbose,而是想知道開到幾級。這時候可以用 count:
parser.add_argument("-v", "--verbose", action="count", default=0)
python job.py
python job.py -v
python job.py -vv
另外,如果你要一次收多個值,可以用 nargs:
parser.add_argument("keywords", nargs="+", help="一個以上關鍵字")
python grep_demo.py error warning critical
常見設定有幾種:
nargs="+",至少一個nargs="*",零個以上nargs=2,固定兩個nargs="?",零或一個
這類設計會讓你的 CLI 比單純硬拆 sys.argv[1:] 乾淨非常多。
3.6 參數名稱和 dest
#
如果你定義了這種旗標:
parser.add_argument("--max-items", type=int, default=10)
在程式裡取得的欄位名稱會自動變成:
args.max_items
因為破折號會轉成底線。如果你想自己指定名稱,也可以加 dest:
parser.add_argument("--max-items", dest="limit", type=int, default=10)
這樣最後拿到的是 args.limit。這個小細節很常用,特別是當 CLI 命名想維持 Unix 風格,但 Python 端又想保留好讀的變數名稱時。
四、讓 help 訊息真的好用 #
4.1 description 和 help 不要省
#
CLI 的第一層 UX 其實就是 help。最基本的兩件事,是替 parser 加 description,替每個 argument 加 help:
parser = argparse.ArgumentParser(description="把 chatPTT 對話紀錄整理成報表")
parser.add_argument("input", help="輸入的 JSON 檔案")
parser.add_argument("--format", choices=["md", "html"], default="md", help="輸出格式")
parser.add_argument("--limit", type=int, default=20, help="最多保留幾筆資料")
help 不用寫成小說,但至少要讓使用者知道這個參數是幹嘛的、有哪些值、預設是什麼。未來的你也會感謝現在的你。
4.2 formatter_class
#
如果你想保留 description 內的換行,可以用 RawTextHelpFormatter。如果你想讓預設值直接顯示在 help 中,可以用 ArgumentDefaultsHelpFormatter。
import argparse
parser = argparse.ArgumentParser(
description="""整理聊天資料並輸出報表
常見流程:
1. 先抓資料
2. 再清理資料
3. 最後輸出 markdown
""",
formatter_class=argparse.RawTextHelpFormatter,
)
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
這些看起來像小功能,但對參數多的工具很有感。help 一旦清楚,工具就會像真的產品,而不是只給原作者自己用的腳本。
4.3 互斥旗標與自訂型別 #
有些參數邏輯上不能同時存在,例如 --quiet 和 --verbose。這時可以用互斥群組:
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("--quiet", action="store_true")
group.add_argument("--verbose", action="store_true")
另外,如果你需要更細的驗證,可以把函式當成 type:
from pathlib import Path
import argparse
def existing_file(value: str) -> Path:
path = Path(value)
if not path.is_file():
raise argparse.ArgumentTypeError(f"找不到檔案: {value}")
return path
parser = argparse.ArgumentParser()
parser.add_argument("input", type=existing_file)
這兩種技巧都很務實,因為它們讓規則留在 parser,而不是散落在程式各處。
五、subcommands 實戰 #
當你的工具只有一件事要做時,單一 parser 很夠用。可是只要功能開始分化,像是 fetch、summarize、export,最好就改用子命令,讓每個動作有自己的參數和 help。
5.1 完整範例 #
下面是一個簡化版的聊天資料工具:
import argparse
from pathlib import Path
def handle_fetch(args):
print(f"fetch from {args.source}, limit={args.limit}")
def handle_summarize(args):
print(f"summarize {args.input} with style={args.style}")
def handle_export(args):
print(f"export {args.input} -> {args.output} ({args.format})")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="chatptt", description="處理 chatPTT 對話資料的小工具")
subparsers = parser.add_subparsers(dest="command", required=True)
fetch_parser = subparsers.add_parser("fetch", help="抓取原始資料")
fetch_parser.add_argument("source", choices=["slack", "discord", "imessage"])
fetch_parser.add_argument("--limit", type=int, default=100)
fetch_parser.set_defaults(func=handle_fetch)
summarize_parser = subparsers.add_parser("summarize", help="摘要對話內容")
summarize_parser.add_argument("input", type=Path)
summarize_parser.add_argument("--style", choices=["brief", "detailed"], default="brief")
summarize_parser.set_defaults(func=handle_summarize)
export_parser = subparsers.add_parser("export", help="匯出整理結果")
export_parser.add_argument("input", type=Path)
export_parser.add_argument("-o", "--output", type=Path, required=True)
export_parser.add_argument("--format", choices=["md", "html"], default="md")
export_parser.set_defaults(func=handle_export)
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
這段最重要的地方有三個。第一,add_subparsers() 會建立子命令系統。第二,每個子命令都能有自己的 arguments。第三,set_defaults(func=...) 可以把 handler 綁到命令上,避免最後變成很長的 if/elif。
5.2 使用方式與好處 #
python chatptt.py fetch slack --limit 30
python chatptt.py summarize logs/chat.json --style detailed
python chatptt.py export output/summary.json -o report.md --format md
這種結構對使用者很友善,因為操作語意清楚。對維護者也很友善,因為不同動作的參數不會全部擠在同一層。當工具慢慢變大時,這個差異會非常明顯。
5.3 子命令 help、Path 與拆分結構
#
子命令也可以有自己的說明:
fetch_parser = subparsers.add_parser(
"fetch",
help="抓取原始資料",
description="從指定資料來源抓取原始聊天紀錄",
)
如果你的工具常處理路徑,直接搭配 Path 通常很舒服:
from pathlib import Path
parser.add_argument("input", type=Path)
另外,拍拍君很推薦把 CLI 結構拆成三層:
build_parser()負責定義 argumentshandle_xxx(args)負責執行邏輯main()負責接線
這樣一來,parser 和商業邏輯不會糊成一坨,測試也更容易寫。
六、進階小技巧與設計原則 #
6.1 BooleanOptionalAction
#
如果某個布林選項需要正反兩種寫法,例如快取預設開啟,但你有時想關掉,可以用 BooleanOptionalAction:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"--cache",
action=argparse.BooleanOptionalAction,
default=True,
help="是否啟用快取",
)
這樣使用者就能寫:
python app.py --cache
python app.py --no-cache
這比只提供 store_true 更完整,尤其在預設值是 True 的情況下特別自然。
6.2 parse_known_args()
#
有些工具本身只是 wrapper,吃掉一部分參數後,把剩下的交給其他程式。這時可以用:
args, remaining = parser.parse_known_args()
這對包裝 pytest、uvicorn 或其他 CLI 很方便。不過如果你不是在做 wrapper,拍手君通常不會優先用它,因為它也可能讓 CLI 規則變得比較模糊。
6.3 設計 CLI 的四個小原則 #
拍拍君自己設計 CLI 時,通常會守四個原則:
- 名稱要直白,少用模糊的
mode、type - 預設值要合理,讓常見情境少打一點字
- help 要站在使用者角度寫,不要只寫內部術語
- 不同動作就拆成 subcommands,不要硬塞成一堆旗標
這些原則很普通,但真的有用。CLI 工具好不好用,往往不是看你用了多炫的框架,而是看參數和 help 設計有沒有清楚。
6.4 什麼時候該升級到 Click 或 Typer? #
拍拍君的建議很簡單:
- 小到中型工具,
argparse通常夠用 - 想用 decorator 和型別註解驅動,去看 Typer
- 想要更成熟的 CLI framework 和延伸生態,去看 Click
argparse 的優勢從來不是華麗,而是標準庫、零依賴、到處都能跑。很多時候,這反而是最務實的選擇。
結語 #
argparse 很樸素,但它真的能把一支零散的 Python script 變成像樣的工具。只要你熟悉 add_argument()、type、default、choices、store_true、nargs、subparsers 這些核心概念,就已經足夠寫出大部分日常需要的 CLI。
如果你最近正想把一些研究腳本、自動化任務或內部小工具整理得更正式,我很推薦從 argparse 開始。先把參數介面、help 訊息、預設值、子命令結構整理乾淨,工具立刻就會從「能跑」升級成「能用」。之後如果需求真的變大,再升級到 Click 或 Typer 也完全不遲。