基于python类型提示构建命令行界面的库。
还在用 argparse 一行行手动解析参数吗?还在为每个脚本写一堆 add_argument、if args.xxx 的样板代码吗? 每次想加个新参数,就要改解析逻辑、改帮助文本、改校验代码,改到最后自己都忘了哪个参数是干嘛的。今天要聊的这位主角——typer,就是来终结这种痛苦的。它让你用 python 类型提示(type hints)直接定义命令行界面,参数解析、帮助文档、类型校验、子命令全部自动生成。写起来像写普通函数,用起来像专业 cli 工具。
1. 为什么是 typer:类型提示驱动的 cli 新姿势
先说清楚 typer 是什么。它是一个基于 python 类型提示构建命令行界面的库,底层封装了另一个老牌库 click,但把 click 那套装饰器堆叠的写法大幅简化了。你只需要写一个带类型注解的普通函数,typer 就能把它变成一个功能完整的命令行工具。
对比一下最直观。用标准库 argparse 写一个加法器:
import argparse
def main():
parser = argparse.argumentparser(description="两数相加")
parser.add_argument("a", type=float, help="第一个数")
parser.add_argument("b", type=float, help="第二个数")
parser.add_argument("--verbose", action="store_true", help="显示详情")
args = parser.parse_args()
result = args.a + args.b
if args.verbose:
print(f"{args.a} + {args.b} = {result}")
else:
print(result)
if __name__ == "__main__":
main()
再看 typer 版本:
import typer
def main(a: float, b: float, verbose: bool = false):
result = a + b
if verbose:
print(f"{a} + {b} = {result}")
else:
print(result)
if __name__ == "__main__":
typer.run(main)
看到区别了吗?没有 argumentparser,没有 add_argument,没有 parse_args。函数签名本身就是接口定义:a 和 b 是位置参数,verbose 因为带默认值自动变成可选选项。类型注解 float 直接变成了参数类型校验,bool 自动变成开关标志。参数名、类型、默认值、帮助信息,全都在一个函数签名里说清楚了。
这就是 typer 的核心哲学:函数签名即 cli 契约。你不需要学习一套新的 dsl,只需要把 python 类型提示用好,typer 负责翻译成命令行语义。对已经习惯写类型注解的现代 python 开发者来说,学习成本几乎为零。
typer 的几个关键卖点:
- 类型提示构建 cli:
int、float、str、bool、path、enum甚至自定义类型都能直接用。 - 自动帮助文档:
--help自动生成,参数说明来自函数参数和typer.argument/typer.option的描述。 - 子命令支持:用
typer()实例注册多个命令,轻松构建git commit、docker run这种多级 cli。 - 参数验证:类型不匹配自动报错,还能配合
min、max、正则等做进一步约束。
2. 安装与环境准备:三行命令搞定
typer 的安装非常轻量,纯 python 实现,没有编译依赖。
pip install typer
如果你想要更丰富的帮助文档渲染(比如带颜色的表格、markdown 格式说明),可以装带 all 扩展的版本:
pip install "typer[all]"
[all] 会额外拉入 rich 和 shellingham。rich 让 --help 输出更漂亮,shellingham 用于自动检测当前 shell 以支持补全提示。
验证安装是否成功:
python -c "import typer; print(typer.__version__)"
预期会打印出版本号,比如 0.12.3。如果没报错,说明环境就绪。
顺便说一句,typer 对 python 版本要求不高,python 3.7 及以上都能用,但建议用 3.8+ 以获得更好的类型提示支持。开发时推荐配合 mypy 或 pyright 做静态检查,因为 typer 的很多能力依赖类型注解的准确性。
3. 核心对象:typer 实例与命令注册
typer 有两个层面的用法。简单模式用 typer.run(),适合单命令脚本;应用模式用 typer.typer() 创建实例,适合多命令工具。先看核心对象。
创建一个 typer 应用:
import typer
app = typer.typer()
@app.command()
def hello(name: str):
print(f"hello {name}")
@app.command()
def goodbye(name: str, formal: bool = false):
if formal:
print(f"goodbye ms. {name}. have a good day.")
else:
print(f"bye {name}!")
if __name__ == "__main__":
app()
这里的 app 就是核心对象。@app.command() 把函数注册成一个子命令,命令名默认取函数名(下划线会转成连字符,比如 say_hello 变成 say-hello)。运行结果:
$ python main.py hello world hello world $ python main.py goodbye world --formal goodbye ms. world. have a good day. $ python main.py --help usage: main.py [options] command [args]... options: --install-completion install completion for the current shell. --show-completion show completion for the current shell, to copy it. --help show this message and exit. commands: goodbye hello
typer() 构造函数还支持一些常用参数:
app = typer.typer(
name="mytool",
help="这是一个演示工具",
add_completion=true, # 是否添加 shell 补全命令
no_args_is_help=true, # 不带参数时显示帮助
rich_markup_mode="rich", # 帮助文档支持 rich 标记
)
no_args_is_help=true 特别实用,用户直接敲命令名不带参数时,会自动展示帮助而不是报错。rich_markup_mode="rich" 则允许你在 help 字符串里用 [bold]、[red] 这类富文本标记。
4. 常用 api 全解析:argument、option 与验证
typer 的 api 不多,但每个都很关键。核心是 typer.argument、typer.option 和 typer.typer。掌握它们,基本就能覆盖 90% 的场景。
argument:位置参数
位置参数就是不带 -- 前缀、按顺序传入的参数。默认情况下,函数里没有默认值的参数就是位置参数。但如果你想加描述、设默认值、做校验,就要显式用 typer.argument:
import typer
def main(
name: str = typer.argument(..., help="用户名字"),
age: int = typer.argument(18, help="年龄,默认18"),
):
print(f"{name} 今年 {age} 岁")
if __name__ == "__main__":
typer.run(main)
... 是 python 的 ellipsis,表示这个参数必填。help 会出现在 --help 里。运行:
$ python main.py 小明 小明 今年 18 岁 $ python main.py 小明 25 小明 今年 25 岁 $ python main.py usage: main.py [options] name [age] try 'main.py --help' for help. error: missing argument 'name'.
option:选项参数
带 -- 前缀的是选项参数。用 typer.option 定义:
import typer
def main(
name: str = typer.option(..., "--name", "-n", help="用户名字"),
age: int = typer.option(18, "--age", "-a", help="年龄"),
vip: bool = typer.option(false, "--vip/--no-vip", help="是否vip"),
):
print(f"name={name}, age={age}, vip={vip}")
if __name__ == "__main__":
typer.run(main)
--name 和 -n 是长短选项,用户可以任选。bool 类型配合 --vip/--no-vip 可以生成一对开关。运行:
$ python main.py -n 小红 -a 20 --vip name=小红, age=20, vip=true $ python main.py --name 小刚 name=小刚, age=18, vip=false
参数验证
typer 的验证能力很实在。类型注解本身就会校验,比如传 abc 给 int 参数会直接报错:
$ python main.py -n 小美 -a abc usage: main.py [options] try 'main.py --help' for help. error: invalid value for '--age' / '-a': 'abc' is not a valid integer.
更进一步,可以用 min、max、regex 等参数:
import typer
def main(
age: int = typer.option(..., min=0, max=150, help="年龄 0-150"),
email: str = typer.option(..., regex=r"^[\w\.-]+@[\w\.-]+\.\w+$", help="邮箱"),
):
print(f"age={age}, email={email}")
if __name__ == "__main__":
typer.run(main)
运行:
$ python main.py --age 200 --email test@example.com error: invalid value for '--age': 200 is not in the range 0<=x<=150. $ python main.py --age 30 --email bad-email error: invalid value for '--email': 'bad-email' does not match the pattern.
enum 类型也天然支持,用户只能从枚举里选:
from enum import enum
import typer
class color(str, enum):
red = "red"
green = "green"
blue = "blue"
def main(color: color = typer.option(color.red, help="颜色")):
print(f"你选了 {color.value}")
if __name__ == "__main__":
typer.run(main)
$ python main.py --color blue 你选了 blue $ python main.py --color yellow error: invalid value for '--color': 'yellow' is not one of 'red', 'green', 'blue'.
path、file、directory 这些类型也能直接用,typer 会自动做路径存在性检查。
5. 完整实战案例:从零写一个文件管理 cli
光说不练假把式。下面我们写一个完整的文件管理工具 fm.py,支持 create、list、delete 三个子命令,带参数校验、帮助文档和确认提示。
import typer
from pathlib import path
from enum import enum
from typing import optional
app = typer.typer(
name="fm",
help="一个简单的文件管理工具",
no_args_is_help=true,
)
class sortby(str, enum):
name = "name"
size = "size"
mtime = "mtime"
@app.command()
def create(
path: path = typer.argument(..., help="要创建的文件路径"),
content: str = typer.option("", "--content", "-c", help="文件内容"),
force: bool = typer.option(false, "--force", "-f", help="覆盖已存在文件"),
):
"""创建一个新文件"""
if path.exists() and not force:
typer.echo(f"文件 {path} 已存在,使用 --force 覆盖")
raise typer.exit(code=1)
path.write_text(content, encoding="utf-8")
typer.echo(f"已创建 {path},写入 {len(content)} 个字符")
@app.command("list")
def list_files(
directory: path = typer.argument(path("."), help="要列出的目录"),
sort: sortby = typer.option(sortby.name, "--sort", "-s", help="排序方式"),
long: bool = typer.option(false, "--long", "-l", help="显示详细信息"),
):
"""列出目录下的文件"""
if not directory.is_dir():
typer.echo(f"{directory} 不是有效目录", err=true)
raise typer.exit(code=1)
files = [f for f in directory.iterdir() if f.is_file()]
if sort == sortby.name:
files.sort(key=lambda f: f.name)
elif sort == sortby.size:
files.sort(key=lambda f: f.stat().st_size)
elif sort == sortby.mtime:
files.sort(key=lambda f: f.stat().st_mtime)
for f in files:
if long:
size = f.stat().st_size
typer.echo(f"{size:>10} {f.name}")
else:
typer.echo(f.name)
@app.command()
def delete(
path: path = typer.argument(..., help="要删除的文件"),
yes: bool = typer.option(false, "--yes", "-y", help="跳过确认"),
):
"""删除文件"""
if not path.exists():
typer.echo(f"文件 {path} 不存在", err=true)
raise typer.exit(code=1)
if not yes:
typer.confirm(f"确定删除 {path} 吗?", abort=true)
path.unlink()
typer.echo(f"已删除 {path}")
if __name__ == "__main__":
app()
这个案例覆盖了 typer 的大部分核心能力。运行效果:
$ python fm.py create demo.txt -c "hello typer"
已创建 demo.txt,写入 11 个字符
$ python fm.py create demo.txt -c "again"
文件 demo.txt 已存在,使用 --force 覆盖
$ python fm.py list . --long
11 demo.txt
1024 fm.py
$ python fm.py delete demo.txt
确定删除 demo.txt 吗? [y/n]: y
已删除 demo.txt
$ python fm.py --help
usage: fm.py [options] command [args]...
一个简单的文件管理工具
options:
--install-completion install completion for the current shell.
--show-completion show completion for the current shell, to copy it.
--help show this message and exit.
commands:
create 创建一个新文件
delete 删除文件
list 列出目录下的文件
几个值得注意的细节:
@app.command("list")显式指定命令名,因为list是 python 内置函数名,函数名改成了list_files。typer.echo是跨平台的输出函数,比print更适合 cli。raise typer.exit(code=1)用于返回非零退出码,方便脚本集成。typer.confirm(..., abort=true)会在用户回答n时自动中止,非常省心。
6. 进阶技巧:回调、上下文与嵌套子命令
当你开始写更复杂的工具时,下面这些技巧会派上大用场。
应用级回调
有时你想在主命令执行前做一些全局初始化,比如读取配置、设置日志级别。用 @app.callback():
import typer
app = typer.typer()
@app.callback()
def main(
verbose: bool = typer.option(false, "--verbose", "-v", help="开启详细日志"),
):
"""全局选项在这里定义"""
if verbose:
typer.echo("详细模式已开启")
@app.command()
def run():
typer.echo("执行任务")
if __name__ == "__main__":
app()
$ python main.py --verbose run 详细模式已开启 执行任务
注意全局选项必须写在子命令前面。
使用 context 传递数据
typer.context 可以在回调与子命令之间传递状态:
import typer
app = typer.typer()
@app.callback()
def main(ctx: typer.context, verbose: bool = typer.option(false, "--verbose")):
ctx.obj = {"verbose": verbose}
@app.command()
def run(ctx: typer.context):
if ctx.obj["verbose"]:
typer.echo("verbose 已开启")
else:
typer.echo("普通模式")
if __name__ == "__main__":
app()
嵌套子命令
大型工具往往需要多级命令,比如 mytool db migrate。用 app.add_typer() 实现:
import typer
app = typer.typer()
db_app = typer.typer()
@db_app.command()
def migrate():
typer.echo("执行数据库迁移")
@db_app.command()
def rollback():
typer.echo("回滚数据库")
app.add_typer(db_app, name="db", help="数据库相关命令")
if __name__ == "__main__":
app()
$ python main.py db migrate 执行数据库迁移 $ python main.py db --help usage: main.py db [options] command [args]... 数据库相关命令 commands: migrate rollback
这种结构非常适合把一个大 cli 拆成多个模块,每个模块一个 typer 实例。
自定义类型与回调
typer 支持通过 callback 参数在参数解析后做转换:
import typer
def parse_port(value: str) -> int:
port = int(value)
if not (1 <= port <= 65535):
raise typer.badparameter("端口必须在 1-65535 之间")
return port
def main(port: int = typer.option(..., callback=parse_port)):
typer.echo(f"端口 {port}")
if __name__ == "__main__":
typer.run(main)
typer.badparameter 会生成友好的错误提示,比直接抛异常优雅得多。
7. 真实工作场景:typer 能帮你解决什么
typer 不是玩具,它在真实项目里非常能打。下面列几个普通开发者马上就能用上的场景。
场景一:数据处理脚本。 你有一堆 csv 要清洗,以前每次改参数都要动代码。现在写一个 cli:
import typer
from pathlib import path
def clean(
src: path = typer.argument(..., exists=true, help="源 csv"),
dst: path = typer.argument(..., help="输出 csv"),
drop_na: bool = typer.option(true, "--drop-na/--keep-na"),
encoding: str = typer.option("utf-8", help="文件编码"),
):
import pandas as pd
df = pd.read_csv(src, encoding=encoding)
if drop_na:
df = df.dropna()
df.to_csv(dst, index=false)
typer.echo(f"已清洗 {len(df)} 行 -> {dst}")
if __name__ == "__main__":
typer.run(clean)
exists=true 让 typer 自动检查源文件是否存在,省掉手写 if not src.exists()。
场景二:项目脚手架。 团队里新建项目要复制一堆模板文件,用 typer 做成 newproj 命令,支持 --template、--name、--git 等选项,新人一条命令就能起步。
场景三:运维小工具。 查日志、重启服务、清理缓存,这些操作封装成 cli 后,配合 typer.confirm 做二次确认,既方便又安全。
场景四:替代 makefile。 很多项目的 makefile 越来越复杂,跨平台还容易出问题。用 typer 写一个 tasks.py,python tasks.py test、python tasks.py lint,可读性和可维护性都更好。
场景五:打包成可执行文件。 配合 pip install 的 entry_points,把 typer 应用注册成全局命令:
# setup.py 或 pyproject.toml
entry_points={
"console_scripts": [
"mytool = mypackage.cli:app",
],
}
安装后直接敲 mytool 就能用,体验和 git、docker 一样。
8. 常见错误与避坑指南
用 typer 久了,总会踩几个坑。这里总结最典型的几个。
坑一:bool 参数想传值却传不进去。 在 typer 里,bool 类型默认是开关标志,不能写成 --flag true。如果你需要显式传值,用 --flag/--no-flag 形式,或者改用 str 再自己解析。
坑二:位置参数和选项参数顺序搞混。 位置参数必须按函数签名顺序传,选项参数可以任意位置。如果一个函数既有多个位置参数又有选项,用户很容易传错。建议位置参数不超过两个,其余都用选项。
坑三:默认值是可变对象。 这个其实是 python 通病,但在 cli 里更隐蔽:
def main(tags: list = typer.option([])): # 危险!
...
应该用 none 做默认值再在函数体内处理:
from typing import optional, list
def main(tags: optional[list[str]] = typer.option(none)):
tags = tags or []
坑四:typer.exit 和 sys.exit 混用。 在 typer 应用里推荐用 raise typer.exit(code=1),它会被 typer 正确处理。直接 sys.exit 有时会绕过 typer 的清理逻辑。
坑五:帮助文本里写了特殊字符。 如果开启了 rich_markup_mode="rich",方括号 [] 会被当成标记解析,想输出字面量方括号要转义成 \[。
坑六:命令名冲突。 如果两个函数名一样(比如都叫 run),后注册的会覆盖前面的。给命令显式命名,或者拆到不同的子 typer 里。
坑七:忘记 if __name__ == "__main__": app()。 这是最常见的低级错误,忘了这行脚本根本不会执行任何命令。
避开这些坑,你的 typer 工具基本就能稳定服役了。
9. 总结:什么时候该用 typer
写到这里,typer 的价值已经很清楚了。它把命令行工具的三大痛点——参数解析、帮助文档、类型校验——全部用 python 类型提示统一解决。你写的是普通函数,得到的是专业 cli。
适合用 typer 的场景:
- 需要快速把脚本变成命令行工具;
- 工具会有多个子命令,结构会越来越复杂;
- 团队里大家都熟悉类型注解,不想学新 dsl;
- 希望自动生成帮助文档和 shell 补全。
不太适合的场景:
- 极致追求启动速度(typer 依赖 click,比裸
argparse略慢,但通常无感); - 需要非常底层的参数解析控制(这时候直接用 click 或 argparse 更合适)。
最后给一个上手建议:从 typer.run() 开始,把你现有的小脚本改造成 typer 版本,感受一下类型提示带来的爽感。然后逐步过渡到 typer.typer(),加上子命令、验证和确认提示。用不了半天,你就会发现自己再也回不去手写 argparse 的日子了。
typer 的官方文档写得很棒,地址是 typer.tiangolo.com,遇到问题先去那里翻一翻,基本都能找到答案。祝你的 cli 之旅愉快!
以上就是python命令行工具typer的用法详解的详细内容,更多关于python命令行工具typer用法的资料请关注代码网其它相关文章!
发表评论