构建 CLI 的 Python 框架:Typer技术介绍

一、什么是 Typer

Typer 是一个基于 Python 类型注解构建 CLI 的框架,底层封装了 Click。它的核心设计理念是:函数签名即命令定义,不需要额外的装饰器参数来声明类型,Python 的类型注解本身就是文档。

安装:

bash 复制代码
pip install typer

二、最小可运行示例

python 复制代码
import typer

app = typer.Typer()

@app.command()
def add(task: str):
    """添加一条待办事项"""
    typer.echo(f"已添加:{task}")

if __name__ == "__main__":
    app()

运行效果:

bash 复制代码
$ python todo.py add "买牛奶"
已添加:买牛奶

$ python todo.py --help
Usage: todo.py [OPTIONS] COMMAND [ARGS]...
Commands:
  add  添加一条待办事项

app = typer.Typer() 创建应用实例。@app.command() 把函数注册为一条命令。函数的 docstring 自动成为 --help 里的说明文字。


三、Argument 与 Option

Typer 通过有没有默认值来区分两种参数:

Argument(位置参数) :没有默认值,用户必须按位置传入,不带 -- 前缀。

Option(选项) :有默认值,用户用 --名称 传入,可以省略。

python 复制代码
@app.command()
def add(
    task: str,                          # 无默认值 → Argument,必填
    priority: int = 2,                  # 有默认值 → Option,可选
    done: bool = False,                 # 有默认值 → Option,可选
):
    """添加待办事项"""
    typer.echo(f"[优先级{priority}] {task}  完成={done}")
bash 复制代码
$ python todo.py add "买牛奶"
[优先级2] 买牛奶  完成=False

$ python todo.py add "买牛奶" --priority 1 --done
[优先级1] 买牛奶  完成=True

bool 类型的 Option 自动变成开关标志,传 --done 就是 True,不传就是 False


四、显式声明 Argument 和 Option

不写默认值时 Typer 自动推断,但显式写出来可以加更多配置:

python 复制代码
@app.command()
def add(
    task: str = typer.Argument(..., help="待办事项内容"),
    priority: int = typer.Option(2, "--priority", "-p", help="优先级 1-3,默认2"),
    tag: str = typer.Option(None, "--tag", "-t", help="标签,如 work/life"),
):
    """添加待办事项"""
    msg = f"[P{priority}] {task}"
    if tag:
        msg += f"  #{tag}"
    typer.echo(msg)
bash 复制代码
$ python todo.py add "买牛奶" -p 1 -t life
[P1] 买牛奶  #life

... 表示必填但没有默认值,等价于不写默认值。"-p" 是短别名,让用户可以用 -p 1 代替 --priority 1None 作为默认值表示"用户没传时为空"。


五、多值参数

接收多个值时使用 List[str]

python 复制代码
from typing import List

@app.command()
def done(
    ids: List[int] = typer.Argument(..., help="要标记完成的任务ID,可传多个"),
):
    """标记多个任务为完成"""
    for i in ids:
        typer.echo(f"任务 #{i} 已完成")
bash 复制代码
$ python todo.py done 1 3 5
任务 #1 已完成
任务 #3 已完成
任务 #5 已完成

六、子命令

一个 CLI 有多条命令时,用 @app.command("名称") 注册不同函数:

python 复制代码
app = typer.Typer()

@app.command("add")
def add(task: str, priority: int = 2):
    """添加待办事项"""
    typer.echo(f"已添加:[P{priority}] {task}")

@app.command("list")
def list_tasks(all: bool = False):
    """列出待办事项"""
    typer.echo("显示所有任务..." if all else "显示未完成任务...")

@app.command("delete")
def delete(id: int):
    """删除待办事项"""
    typer.echo(f"已删除任务 #{id}")
bash 复制代码
$ python todo.py add "买牛奶" --priority 1
$ python todo.py list --all
$ python todo.py delete 3
$ python todo.py --help        # 列出所有子命令

只有一个 @app.command() 时不需要子命令名,直接 python todo.py 就能运行。有多个时必须指定子命令名。


七、输出与颜色

typer.echo 是推荐的输出方式,比 print 多两个能力:

python 复制代码
@app.command("add")
def add(task: str):
    # 普通输出 → stdout
    typer.echo(f"正在添加:{task}")

    # 错误输出 → stderr(不会污染管道)
    typer.echo("警告:任务内容过长", err=True)

    # 带颜色输出
    typer.echo(typer.style("✓ 添加成功", fg=typer.colors.GREEN, bold=True))
    typer.echo(typer.style("✗ 添加失败", fg=typer.colors.RED))
    typer.echo(typer.style("⚠ 注意", fg=typer.colors.YELLOW))

err=True 很重要:当用户用管道 python todo.py list | grep 牛奶 时,错误信息输出到 stderr,不会混进 grep 的输入里。


八、错误处理与退出

不要用 sys.exit(),用 Typer 提供的方式:

python 复制代码
@app.command("delete")
def delete(id: int):
    """删除待办事项"""
    if id <= 0:
        typer.echo(
            typer.style(f"错误:ID 必须大于 0,收到 {id}", fg=typer.colors.RED),
            err=True
        )
        raise typer.Exit(code=1)    # 退出码 1 表示失败,不打印堆栈

    # 模拟确认操作
    confirmed = typer.confirm(f"确认删除任务 #{id}?")
    if not confirmed:
        raise typer.Abort()         # 打印 "Aborted!" 并退出

    typer.echo(f"已删除任务 #{id}")
bash 复制代码
$ python todo.py delete -1
错误:ID 必须大于 0,收到 -1

$ python todo.py delete 3
确认删除任务 #3? [y/N]: n
Aborted!

Exit(code=1) 用于程序检测到错误主动退出,Abort() 用于用户取消操作。两者都不会打印 Python 堆栈。


九、进度条

处理耗时操作时,用内置进度条给用户反馈:

python 复制代码
import time

@app.command("sync")
def sync():
    """同步所有待办事项到云端"""
    tasks = ["任务A", "任务B", "任务C", "任务D"]

    with typer.progressbar(tasks, label="同步中") as progress:
        for task in progress:
            time.sleep(0.5)   # 模拟网络请求

    typer.echo(typer.style("✓ 同步完成", fg=typer.colors.GREEN))
bash 复制代码
$ python todo.py sync
同步中  ████████████████████  100%
✓ 同步完成

十、全局选项与版本号

@app.callback() 给整个 CLI 加全局选项,比如 --version

python 复制代码
def version_callback(value: bool):
    if value:
        typer.echo("todo-cli v1.0.0")
        raise typer.Exit()

@app.callback()
def main(
    version: bool = typer.Option(
        None,
        "--version", "-v",
        callback=version_callback,
        is_eager=True,
        help="显示版本号",
    )
):
    """待办事项管理工具"""
    pass
bash 复制代码
$ python todo.py --version
todo-cli v1.0.0

$ python todo.py -v
todo-cli v1.0.0

is_eager=True--version 优先于其他所有参数处理,否则如果其他必填参数没传,会先报错再处理 --version,体验很差。


十一、完整示例

把以上所有概念组合成一个完整的 todo.py

python 复制代码
#!/usr/bin/env python3
"""待办事项 CLI --- Typer 完整示例"""

import time
from typing import List
import typer

app = typer.Typer(help="待办事项管理工具")


def version_callback(value: bool):
    if value:
        typer.echo("todo-cli v1.0.0")
        raise typer.Exit()


@app.callback()
def main(
    version: bool = typer.Option(
        None, "--version", "-v",
        callback=version_callback,
        is_eager=True,
        help="显示版本号",
    )
):
    pass


@app.command("add")
def add(
    task: str = typer.Argument(..., help="待办事项内容"),
    priority: int = typer.Option(2, "--priority", "-p", help="优先级 1-3"),
    tag: str = typer.Option(None, "--tag", "-t", help="标签"),
):
    """添加一条待办事项"""
    msg = f"[P{priority}] {task}"
    if tag:
        msg += f"  #{tag}"
    typer.echo(typer.style("✓ " + msg, fg=typer.colors.GREEN))


@app.command("done")
def done(
    ids: List[int] = typer.Argument(..., help="要标记完成的任务ID"),
):
    """标记任务为完成"""
    for i in ids:
        typer.echo(f"任务 #{i} 已完成")


@app.command("delete")
def delete(
    id: int = typer.Argument(..., help="要删除的任务ID"),
    force: bool = typer.Option(False, "--force", "-f", help="跳过确认"),
):
    """删除一条待办事项"""
    if id <= 0:
        typer.echo(typer.style(f"错误:无效ID {id}", fg=typer.colors.RED), err=True)
        raise typer.Exit(code=1)

    if not force:
        confirmed = typer.confirm(f"确认删除任务 #{id}?")
        if not confirmed:
            raise typer.Abort()

    typer.echo(f"已删除任务 #{id}")


@app.command("sync")
def sync():
    """同步到云端"""
    tasks = ["读取本地数据", "连接服务器", "上传变更", "确认同步"]
    with typer.progressbar(tasks, label="同步中") as progress:
        for _ in progress:
            time.sleep(0.3)
    typer.echo(typer.style("✓ 同步完成", fg=typer.colors.GREEN))


if __name__ == "__main__":
    app()
bash 复制代码
$ python todo.py --help
$ python todo.py add "买牛奶" -p 1 -t life
$ python todo.py done 1 3 5
$ python todo.py delete 2
$ python todo.py delete 2 --force
$ python todo.py sync
$ python todo.py --version

十二、Typer 与 Click 的关系

Typer 是 Click 的上层封装。两者的核心差异是参数声明方式:Click 需要用装饰器手动声明每个参数的类型,Typer 直接读取函数的类型注解。

python 复制代码
# Click:类型在装饰器里声明
@click.command()
@click.argument("task")
@click.option("--priority", type=int, default=2, help="优先级")
def add(task, priority):
    ...

# Typer:类型注解即声明,代码量减少约 40%
@app.command()
def add(task: str, priority: int = 2):
    ...

Typer 的 app 本质上是一个 Click Group,所以两者可以混用,也可以把 Typer 的 app 嵌入到 Click 的命令树里。

相关推荐
AbandonForce2 小时前
STL list
开发语言·c++
前端老石人2 小时前
HTML 入门指南:从规范视角建立正确知识体系
开发语言·前端·html
nimadan122 小时前
豆包写小说软件2025推荐,专业写作助力灵感迸发
大数据·人工智能·python
沐知全栈开发2 小时前
MySQL 索引
开发语言
Albert Edison2 小时前
【C++11】特殊类设计
开发语言·c++·单例模式·饿汉模式·懒汉模式
代码改善世界2 小时前
【C++初阶】vector 核心接口和模拟实现
开发语言·c++
Lyyaoo.2 小时前
【设计模式】工厂模式
java·开发语言·设计模式
I love studying!!!2 小时前
Web项目:从Django入手
后端·python·django
宵时待雨2 小时前
C++笔记归纳20:智能指针
开发语言·c++·笔记