推荐用 Typer 或 Click 替代裸 argparse,因其通过类型注解和 docstring 自动生成 CLI;用 Pydantic Settings 统一管理配置优先级;按功能拆分 CLI 子命令;并规范错误处理与退出码。

为什么不用 argparse 手写参数解析
手写 sys.argv 解析或用裸 argparse 搭建 CLI,短期快,长期疼。常见问题包括:帮助文本和实际行为不一致、子命令嵌套三层后逻辑散落在各处、类型校验靠 type=int 但错误提示不友好、缺少默认值文档化能力。
推荐直接用 typer 或 click——它们把函数签名转成 CLI 接口,参数类型、默认值、help 文本全由 Python 类型注解和 docstring 驱动,改代码即改 CLI 行为。
-
typer更轻量,适合中小型工具,Optional[str]自动映射为可选参数,Path类型自动做路径存在性检查 -
click生态更成熟,适合需要自定义 shell 补全、多级 group、或集成 Flask/Django 的场景 - 避免混合使用:比如在
typer里手动调argparse.ArgumentParser.add_argument,会绕过类型系统,导致 help 文本和运行时行为脱节
如何让 CLI 支持配置文件 + 环境变量 + 命令行优先级
用户不会只靠 --host localhost --port 8080 启动服务;他们要 export MYAPP_PORT=3000,或写 config.yaml,还要能被命令行覆盖。硬编码优先级容易出错,比如环境变量覆盖了配置文件却没覆盖命令行。
用 pydantic.BaseSettings(v1)或 pydantic-settings(v2)统一管理:
立即学习“Python免费学习笔记(深入)”;
- 定义一个
Settings类,字段带默认值和Field(env="MYAPP_HOST") - 配置文件路径通过
_env_file参数指定,支持.env、pyproject.toml、config.yaml多种格式 - 命令行参数仍走
typer或click,最后用Settings().model_dump()合并所有来源,明确知道哪一层赢了
注意:不要在 Settings 初始化时就触发敏感操作(如连接数据库),它只是配置容器;真正使用时再实例化。
子命令太多时怎么避免 main.py 膨胀成意大利面条
当 CLI 出现 mytool db migrate、mytool api serve、mytool export csv 时,把所有逻辑塞进一个文件会导致导入循环、测试难写、IDE 跳转卡顿。
本文档主要讲述的是Python概述;Python 对操作系统服务的内置接口,使其成为编写可移植的维护操作系统的管理工具和部件(有时也被称为Shell 工具)的理想工具。Python 程序可以搜索文件和目录树,可以运行其他程序,用进程或线程进行并行处理等等。希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看
按功能拆包,结构类似:
mytool/ ├── __main__.py # 只有 CLI 入口,import mytool.cli ├── cli/ │ ├── __init__.py # 定义 Typer() 实例,add_typer(db_app), add_typer(api_app) │ ├── db.py # 所有 db 相关命令,含独立测试 fixture │ └── api.py # 同上 └── core/ # 业务逻辑,不 import cli 模块
关键点:
-
cli/__init__.py不实现逻辑,只组装命令树;每个*.py文件导出自己的Typer实例(如db_app = Typer()) - 测试时直接
from mytool.cli.db import db_app,用db_app.invoke(...)测试子命令,不启动整个 CLI - 禁止
cli/db.py导入cli/api.py,跨命令复用逻辑必须下沉到core/
日志、错误、退出码怎么才算“对用户友好”
CLI 不是脚本,用户可能把它写进 cron、管道或 CI。打印 Exception: xxx 或静默失败,都会让自动化流程难以诊断。
必须做三件事:
- 捕获顶层异常,用
typer.echo(f"[error] {e}", err=True)输出到 stderr,并调用raise typer.Exit(1);不要用sys.exit(1),它绕过typer的清理逻辑 - 日志级别分清:
INFO给用户看进度(如 “Uploading 3 files…”),DEBUG给开发者查问题(含完整 trace、SQL、HTTP headers),用LOG_LEVEL=debug mytool ...控制 - 每个有意义的操作返回不同退出码:
0成功,1通用错误,2参数错误(typer默认),3连接失败,4认证失败——Shell 脚本才能case $? in 3) retry;;
最常被忽略的是:退出码语义必须写进 --help 或 README,否则别人根本不知道 3 代表什么。









