Skip to content
话题
Python
Python Argparse:如何正确构建命令行接口

Python Argparse:如何正确构建命令行接口

更新于

你写了一个 Python 脚本,它能按照你的需求精确处理数据。然后你的同事想拿去用。“你只要改第 14 行的文件名和第 37 行的阈值就行了,”你告诉他们。他们却改错了行。脚本坏了。你花了 20 分钟去调试别人对你正常工作的代码所做的修改。每当有人需要调整某个参数时,都会发生这种事,而且随着脚本变大,问题会越来越严重。把硬编码值写进脚本里并不能扩展 -- 无论是对团队、自动化还是生产环境都不行。

Python 的 argparse 模块通过把任意脚本变成一个正规的命令行工具来解决这个问题。用户在运行脚本时传入参数。该模块负责解析、类型转换、校验以及帮助信息生成。它包含在标准库中,因此无需安装。本指南将带你了解 argparse 的所有必要特性 -- 从基础的位置参数到子命令和真实世界的 CLI 设计模式。

argparse 的作用,以及它为什么比 sys.argv 更好

每个 Python 脚本都可以访问 sys.argv,它是来自命令行的原始字符串列表。你可以手动解析它:

import sys
 
filename = sys.argv[1]
threshold = float(sys.argv[2])
verbose = "--verbose" in sys.argv

这对于一次性脚本还行,但很快就会崩掉。没有帮助信息。没有类型校验。用户忘记参数时也没有错误提示。也无法正确处理可选标志。只要你增删一个参数,索引就会立刻失效。

argparse 通过声明式 API 解决了所有这些问题。你定义脚本接受哪些参数,剩下的都交给 argparse:

import argparse
 
parser = argparse.ArgumentParser(description="Process data files")
parser.add_argument("filename", help="Input file to process")
parser.add_argument("--threshold", type=float, default=0.5, help="Detection threshold")
parser.add_argument("--verbose", action="store_true", help="Enable detailed output")
args = parser.parse_args()
 
print(f"Processing {args.filename} with threshold {args.threshold}")

你会自动得到这些能力:

  • 自动 --help:运行 python script.py --help,用户就能看到每个参数、类型以及默认值。
  • 类型转换--threshold 0.8 会自动变成浮点数。如果有人传入 --threshold abc,argparse 会打印清晰的错误。
  • 校验:缺少必需参数时会给出友好的错误消息,而不是晦涩的 IndexError 回溯。
  • 灵活顺序:可选参数可以以任意顺序出现。--verbose --threshold 0.8--threshold 0.8 --verbose 效果相同。

下面是帮助信息的样子:

$ python script.py --help
usage: script.py [-h] [--threshold THRESHOLD] [--verbose] filename
 
Process data files
 
positional arguments:
  filename              Input file to process
 
options:
  -h, --help            show this help message and exit
  --threshold THRESHOLD Detection threshold
  --verbose             Enable detailed output

你的第一个 CLI 脚本

我们从零开始构建一个完整可运行的脚本。这个工具会根据名字向用户问好,并可选地重复问候若干次。

#!/usr/bin/env python3
"""greet.py -- A simple greeting CLI tool."""
 
import argparse
 
def main():
    parser = argparse.ArgumentParser(
        prog="greet",
        description="Greet someone by name",
    )
    parser.add_argument("name", help="The person to greet")
    parser.add_argument(
        "-c", "--count",
        type=int,
        default=1,
        help="Number of times to greet (default: 1)",
    )
    parser.add_argument(
        "-u", "--uppercase",
        action="store_true",
        help="Print greeting in uppercase",
    )
 
    args = parser.parse_args()
 
    greeting = f"Hello, {args.name}!"
    if args.uppercase:
        greeting = greeting.upper()
 
    for _ in range(args.count):
        print(greeting)
 
if __name__ == "__main__":
    main()

在终端中测试它:

$ python greet.py Alice
Hello, Alice!
 
$ python greet.py Bob --count 3
Hello, Bob!
Hello, Bob!
Hello, Bob!
 
$ python greet.py Charlie -c 2 -u
HELLO, CHARLIE!
HELLO, CHARLIE!
 
$ python greet.py
usage: greet [-h] [-c COUNT] [-u] name
greet: error: the following arguments are required: name

注意这个脚本有三点:

  1. 解析逻辑放在 main(),而不是模块顶层。这样脚本可以被导入,而不会触发参数解析。
  2. if __name__ == "__main__" 保护 确保 main() 只在脚本直接执行时运行,而不是被导入时运行。
  3. 短参数和长参数-c--count)让用户可以在简洁与清晰之间选择。

这三个模式会出现在每一个写得好的 argparse 脚本中。从一开始就遵循它们。

位置参数

位置参数在定义时不带短横线。它们默认是必需的,并按命令行中的位置匹配。

import argparse
 
parser = argparse.ArgumentParser(description="Copy files")
parser.add_argument("source", help="Source file path")
parser.add_argument("destination", help="Destination file path")
args = parser.parse_args()
 
print(f"Copying {args.source} -> {args.destination}")
$ python copy.py report.csv /backup/report.csv
Copying report.csv -> /backup/report.csv

使用 nargs 接收多个位置值

nargs 参数控制一个参数消耗多少个值:

import argparse
 
parser = argparse.ArgumentParser(description="Merge files")
 
# One or more files (at least one required)
parser.add_argument("files", nargs="+", help="Files to merge")
 
# Exactly two values
parser.add_argument("--range", nargs=2, type=int, metavar=("START", "END"),
                    help="Row range to extract")
 
args = parser.parse_args()
print(f"Merging: {args.files}")
if args.range:
    print(f"Range: {args.range[0]} to {args.range[1]}")
$ python merge.py data1.csv data2.csv data3.csv --range 10 500
Merging: ['data1.csv', 'data2.csv', 'data3.csv']
Range: 10 to 500

下面是完整的 nargs 参考表:

nargs valueMeaningResult typeExample
(omitted)Exactly one valueSingle value"input.csv"
N (integer)Exactly N valuesList of N items[10, 100]
"?"Zero or one valueSingle value or default"config.yml" or None
"*"Zero or more valuesList (possibly empty)[] or ["a", "b"]
"+"One or more valuesList (error if empty)["a"] or ["a", "b"]
argparse.REMAINDERAll remaining argsListEverything after this arg

对位置参数做类型转换

位置参数默认是字符串。添加 type 可以把它们转换成别的类型:

import argparse
 
parser = argparse.ArgumentParser(description="Calculate rectangle area")
parser.add_argument("width", type=float, help="Rectangle width")
parser.add_argument("height", type=float, help="Rectangle height")
args = parser.parse_args()
 
area = args.width * args.height
print(f"Area: {area:.2f}")
$ python area.py 3.5 7.2
Area: 25.20
 
$ python area.py three 7.2
usage: area.py [-h] width height
area.py: error: argument width: invalid float value: 'three'

错误信息是自动生成的,并且会明确告诉用户哪里出了问题。

可选参数

可选参数以 -(短格式)或 --(长格式)开头。它们默认是可选的,并且可以按任意顺序出现。

import argparse
 
parser = argparse.ArgumentParser(description="Data exporter")
parser.add_argument("-o", "--output", default="output.csv", help="Output file path")
parser.add_argument("-d", "--delimiter", default=",", help="Field delimiter")
parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed progress")
parser.add_argument("-q", "--quiet", action="store_true", help="Suppress all output")
 
args = parser.parse_args()
print(f"Output: {args.output}")
print(f"Delimiter: {repr(args.delimiter)}")
print(f"Verbose: {args.verbose}")
$ python export.py --output results.tsv --delimiter "\t" --verbose
Output: results.tsv
Delimiter: '\\t'
Verbose: True
 
$ python export.py -o results.json -v
Output: results.json
Delimiter: ','
Verbose: True

默认值

每个可选参数都有一个默认值。如果你没有显式设置,它默认为 None

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--name", default="World")      # Default: "World"
parser.add_argument("--port", type=int, default=8080) # Default: 8080
parser.add_argument("--log-file")                     # Default: None
 
args = parser.parse_args()
print(f"Name: {args.name}, Port: {args.port}, Log: {args.log_file}")

注意:当参数名包含短横线(如 --log-file)时,argparse 会把它转换成属性名里的下划线:args.log_file

store_true 和 store_false 动作

布尔标志不接收值。它们要么存在,要么不存在:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", action="store_true", help="Enable verbose mode")
parser.add_argument("--no-cache", action="store_true", help="Disable caching")
parser.add_argument("--dry-run", action="store_true", help="Preview without executing")
 
args = parser.parse_args()
# --verbose present -> args.verbose is True
# --verbose absent  -> args.verbose is False

用 count 动作表示 verbosity 等级

有些工具使用重复标志来表示详细程度(-v-vv-vvv):

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", action="count", default=0,
                    help="Increase verbosity (-v, -vv, -vvv)")
args = parser.parse_args()
 
if args.verbose >= 3:
    print("TRACE level: showing everything")
elif args.verbose >= 2:
    print("DEBUG level: detailed output")
elif args.verbose >= 1:
    print("INFO level: progress updates")
else:
    print("Default: errors only")
$ python tool.py -vvv
TRACE level: showing everything

用 append 动作接收重复参数

当用户可以多次指定同一个标志时,使用 action="append"

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--tag", action="append", default=[],
                    help="Add a tag (can be repeated)")
args = parser.parse_args()
print(f"Tags: {args.tag}")
$ python tool.py -t bug -t urgent -t backend
Tags: ['bug', 'urgent', 'backend']

类型和取值范围

内置类型转换

type 参数可以接受任何一个以字符串为输入并返回某个值的可调用对象。你可以使用内置类型、pathlib.Path 处理文件路径,或者自定义函数:

import argparse
from pathlib import Path
 
parser = argparse.ArgumentParser()
parser.add_argument("--count", type=int, help="Number of items")
parser.add_argument("--rate", type=float, help="Processing rate")
parser.add_argument("--config", type=Path, help="Config file path")
args = parser.parse_args()

使用 choices 限制取值

choices 把参数限制在特定可选值内:

import argparse
 
parser = argparse.ArgumentParser(description="Deploy application")
parser.add_argument("environment", choices=["dev", "staging", "production"],
                    help="Target environment")
parser.add_argument("--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR"],
                    default="INFO", help="Logging level")
parser.add_argument("--replicas", type=int, choices=range(1, 11),
                    default=1, help="Number of replicas (1-10)")
args = parser.parse_args()
$ python deploy.py testing
usage: deploy.py [-h] {dev,staging,production}
deploy.py: error: argument environment: invalid choice: 'testing'
  (choose from 'dev', 'staging', 'production')

自定义类型函数

自定义类型函数是 argparse 最强大的特性之一。它们允许你在解析阶段就完成校验和转换:

import argparse
from datetime import datetime
 
def positive_int(value):
    """Accept only positive integers."""
    ivalue = int(value)
    if ivalue <= 0:
        raise argparse.ArgumentTypeError(f"{value} is not a positive integer")
    return ivalue
 
def date_type(value):
    """Parse YYYY-MM-DD date strings."""
    try:
        return datetime.strptime(value, "%Y-%m-%d").date()
    except ValueError:
        raise argparse.ArgumentTypeError(
            f"Invalid date: '{value}'. Expected format: YYYY-MM-DD"
        )
 
def percentage(value):
    """Accept floats between 0 and 100."""
    fvalue = float(value)
    if not 0 <= fvalue <= 100:
        raise argparse.ArgumentTypeError(
            f"{value} is not a valid percentage (0-100)"
        )
    return fvalue
 
parser = argparse.ArgumentParser()
parser.add_argument("--workers", type=positive_int, default=4)
parser.add_argument("--since", type=date_type, help="Start date (YYYY-MM-DD)")
parser.add_argument("--sample", type=percentage, default=100.0,
                    help="Sample percentage (0-100)")
args = parser.parse_args()
$ python tool.py --workers -3
error: argument --workers: -3 is not a positive integer
 
$ python tool.py --since 2026-13-45
error: argument --since: Invalid date: '2026-13-45'. Expected format: YYYY-MM-DD
 
$ python tool.py --sample 150
error: argument --sample: 150 is not a valid percentage (0-100)

每个校验错误都会给出清晰、可操作的消息,而你不需要在主代码里写任何 if/else 逻辑。

必需的可选参数

你可以强制一个可选参数变成必填:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--config", required=True, help="Path to config file")
parser.add_argument("--output", required=True, help="Output directory")
parser.add_argument("--format", default="json", help="Output format")
args = parser.parse_args()
$ python tool.py --format csv
usage: tool.py [-h] --config CONFIG --output OUTPUT [--format FORMAT]
tool.py: error: the following arguments are required: --config, --output

为什么这有时像代码味道:如果每个“可选”参数其实都必须传,那么你可能应该改用位置参数。只有当参数名本身能提升可读性(例如 --config 比一个裸路径更清楚)或者当你有很多必填参数、且命名参数能防止顺序错误时,required optional 才更合适。

更适合真正必填输入的模式通常是位置参数:

# Instead of --config (required optional)
parser.add_argument("config", help="Path to config file")
 
# Use required optionals when the name matters
parser.add_argument("--api-key", required=True, help="API authentication key")

互斥组

有时参数彼此冲突。某个脚本可能只能输出 JSON 或 CSV,不能两者同时输出。add_mutually_exclusive_group() 可以强制这种约束:

import argparse
 
parser = argparse.ArgumentParser(description="Export data")
 
# Output format: pick exactly one
format_group = parser.add_mutually_exclusive_group(required=True)
format_group.add_argument("--json", action="store_true", help="Export as JSON")
format_group.add_argument("--csv", action="store_true", help="Export as CSV")
format_group.add_argument("--parquet", action="store_true", help="Export as Parquet")
 
# Verbosity: pick at most one
verbosity = parser.add_mutually_exclusive_group()
verbosity.add_argument("-v", "--verbose", action="store_true", help="Detailed output")
verbosity.add_argument("-q", "--quiet", action="store_true", help="Minimal output")
 
parser.add_argument("input_file", help="Input data file")
args = parser.parse_args()
 
if args.json:
    print(f"Exporting {args.input_file} as JSON")
elif args.csv:
    print(f"Exporting {args.input_file} as CSV")
elif args.parquet:
    print(f"Exporting {args.input_file} as Parquet")
$ python export.py data.csv --json --csv
error: argument --csv: not allowed with argument --json
 
$ python export.py data.csv
error: one of the arguments --json --csv --parquet is required
 
$ python export.py data.csv --parquet -v
Exporting data.csv as Parquet

另一种更实用的格式选择方式,是用 choices 而不是多个标志:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--format", choices=["json", "csv", "parquet"],
                    required=True, help="Output format")
args = parser.parse_args()

两种方式都可行。若每个选项还需要自己的额外参数,互斥组更合适;如果这些选项只是简单字符串,choices 更好。

使用 add_subparsers 添加子命令

真实世界里的 CLI 工具通常都使用子命令。比如 git commitdocker buildpip install。每个子命令都有自己的参数、自己的帮助文本,以及自己的处理函数。argparse 通过 add_subparsers() 支持这种模式:

#!/usr/bin/env python3
"""project.py -- A project management CLI tool."""
 
import argparse
 
def handle_init(args):
    """Initialize a new project."""
    print(f"Creating project '{args.name}'")
    print(f"  Template: {args.template}")
    print(f"  Directory: {args.name}/")
 
def handle_build(args):
    """Build the project."""
    mode = "release" if args.optimize else "debug"
    print(f"Building in {mode} mode")
    if args.target:
        print(f"  Target: {args.target}")
 
def handle_test(args):
    """Run tests."""
    print(f"Running tests (coverage={'on' if args.coverage else 'off'})")
    if args.pattern:
        print(f"  Pattern: {args.pattern}")
 
def handle_deploy(args):
    """Deploy the project."""
    print(f"Deploying to {args.environment}")
    if args.dry_run:
        print("  (DRY RUN -- no actual deployment)")
 
def main():
    # Top-level parser
    parser = argparse.ArgumentParser(
        prog="project",
        description="Manage your project lifecycle",
    )
    parser.add_argument("--version", action="version", version="project 1.0.0")
 
    subparsers = parser.add_subparsers(dest="command", required=True,
                                        help="Available commands")
 
    # init subcommand
    init_cmd = subparsers.add_parser("init", help="Create a new project")
    init_cmd.add_argument("name", help="Project name")
    init_cmd.add_argument("--template", default="basic",
                          choices=["basic", "web", "api", "ml"],
                          help="Project template (default: basic)")
    init_cmd.set_defaults(func=handle_init)
 
    # build subcommand
    build_cmd = subparsers.add_parser("build", help="Build the project")
    build_cmd.add_argument("--optimize", action="store_true",
                           help="Enable release optimizations")
    build_cmd.add_argument("--target", help="Build target platform")
    build_cmd.set_defaults(func=handle_build)
 
    # test subcommand
    test_cmd = subparsers.add_parser("test", help="Run tests")
    test_cmd.add_argument("--coverage", action="store_true",
                          help="Generate coverage report")
    test_cmd.add_argument("--pattern", "-p", help="Test name pattern to match")
    test_cmd.set_defaults(func=handle_test)
 
    # deploy subcommand
    deploy_cmd = subparsers.add_parser("deploy", help="Deploy the project")
    deploy_cmd.add_argument("environment",
                            choices=["staging", "production"],
                            help="Target environment")
    deploy_cmd.add_argument("--dry-run", action="store_true",
                            help="Preview without deploying")
    deploy_cmd.set_defaults(func=handle_deploy)
 
    args = parser.parse_args()
    args.func(args)
 
if __name__ == "__main__":
    main()
$ python project.py init myapp --template web
Creating project 'myapp'
  Template: web
  Directory: myapp/
 
$ python project.py build --optimize
Building in release mode
 
$ python project.py test --coverage -p "test_api*"
Running tests (coverage=on)
  Pattern: test_api*
 
$ python project.py deploy production --dry-run
Deploying to production
  (DRY RUN -- no actual deployment)
 
$ python project.py --help
usage: project [-h] [--version] {init,build,test,deploy} ...
 
Manage your project lifecycle
 
positional arguments:
  {init,build,test,deploy}
                        Available commands
    init                Create a new project
    build               Build the project
    test                Run tests
    deploy              Deploy the project
 
$ python project.py deploy --help
usage: project deploy [-h] [--dry-run] {staging,production}
 
positional arguments:
  {staging,production}  Target environment
 
options:
  -h, --help            show this help message and exit
  --dry-run             Preview without deploying

这里的关键模式是 set_defaults(func=handler)。每个子命令都会把自己的处理函数存进解析后的命名空间,然后主函数通过 args.func(args) 分发执行。这是生产级 CLI 工具中最常见的标准方式。

用参数组改善帮助信息

当工具有很多参数时,可以把它们按逻辑分组:

import argparse
 
parser = argparse.ArgumentParser(description="Data pipeline tool")
 
input_group = parser.add_argument_group("Input options")
input_group.add_argument("--source", required=True, help="Data source path")
input_group.add_argument("--format", choices=["csv", "json", "parquet"], default="csv")
input_group.add_argument("--encoding", default="utf-8", help="File encoding")
 
transform_group = parser.add_argument_group("Transform options")
transform_group.add_argument("--filter", help="Filter expression")
transform_group.add_argument("--sort-by", help="Column to sort by")
transform_group.add_argument("--limit", type=int, help="Max rows to process")
 
output_group = parser.add_argument_group("Output options")
output_group.add_argument("-o", "--output", required=True, help="Output path")
output_group.add_argument("--compress", action="store_true", help="Compress output")
 
args = parser.parse_args()

--help 输出会按这些标题对参数进行分组,让长参数列表更易读。这纯粹是展示层面的功能 -- 不会改变参数解析方式。

自定义帮助信息和格式

formatter_class 参数

import argparse
 
parser = argparse.ArgumentParser(
    prog="analyzer",
    description="Analyze datasets and generate reports.\n\n"
                "Supports CSV, JSON, and Parquet input formats.\n"
                "Output can be filtered, sorted, and aggregated.",
    epilog="Examples:\n"
           "  analyzer data.csv --format json --top 10\n"
           "  analyzer data.csv --columns name age --sort-by age\n"
           "  analyzer data.csv --filter 'age > 30' --output results.csv",
    formatter_class=argparse.RawDescriptionHelpFormatter,
)
FormatterWhat it does
HelpFormatterDefault. Wraps text to fit terminal width.
RawDescriptionHelpFormatterPreserves newlines in description and epilog.
RawTextHelpFormatterPreserves newlines everywhere, including argument help.
ArgumentDefaultsHelpFormatterAppends (default: X) to every argument's help text.

metavar 和 %(default)s

控制使用说明中显示的内容:

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument("--top", type=int, default=10, metavar="N",
                    help="Show top N results (default: %(default)s)")
parser.add_argument("--format", default="table", metavar="FMT",
                    help="Output format (default: %(default)s)")

这会让 usage 文本中显示 --top N 而不是 --top TOP,并且帮助文本会显示真实的默认值。

argparse vs sys.argv vs click vs typer

Python 提供了多种命令行参数解析方式。下面是它们的对比:

Featuresys.argvargparseclicktyper
Part of stdlibYesYesNo (pip install)No (pip install)
Type conversionManualBuilt-inBuilt-inAutomatic (type hints)
Help generationNoneAutomaticAutomaticAutomatic
SubcommandsManualadd_subparsers()@group.command()app.command()
ValidationManualchoices, custom typeclick.Choice, callbacksValidators
BoilerplateHighMediumLowVery low
API styleImperativeImperativeDecoratorsDecorators + type hints
Tab completionNoneNoneVia pluginBuilt-in
Prompting / colorsManualNoneBuilt-inBuilt-in
TestingManualparse_args([...])CliRunnerCliRunner
Best forThrowaway scriptsStdlib-only projectsComplex CLIsModern Python 3.7+

下面是同一个工具用不同方式实现的版本:

# --- sys.argv: raw, fragile, no help ---
import sys
 
name = sys.argv[1] if len(sys.argv) > 1 else "World"
count = int(sys.argv[2]) if len(sys.argv) > 2 else 1
for _ in range(count):
    print(f"Hello, {name}!")
# --- argparse: stdlib, explicit argument definitions ---
import argparse
 
parser = argparse.ArgumentParser(description="Greet someone")
parser.add_argument("name", nargs="?", default="World", help="Name to greet")
parser.add_argument("-c", "--count", type=int, default=1, help="Repetitions")
args = parser.parse_args()
for _ in range(args.count):
    print(f"Hello, {args.name}!")
# --- click: decorators, pip install click ---
import click
 
@click.command()
@click.argument("name", default="World")
@click.option("-c", "--count", default=1, type=int, help="Repetitions")
def greet(name, count):
    """Greet someone."""
    for _ in range(count):
        click.echo(f"Hello, {name}!")
 
greet()
# --- typer: type hints, pip install typer ---
import typer
 
def greet(name: str = "World", count: int = 1):
    """Greet someone."""
    for _ in range(count):
        print(f"Hello, {name}!")
 
typer.run(greet)

何时使用哪一种:

  • sys.argv -- 只适合一次性脚本:你只取一个值,然后不需要校验。
  • argparse -- 当你需要一个真正的 CLI,但又不能加入外部依赖时。对于大多数项目来说,这是默认的正确选择。
  • click -- 当你在构建复杂 CLI,需要嵌套命令组、交互提示、彩色输出以及插件系统时。
  • typer -- 当你想要最少样板代码,并且使用 Python 3.7+ 时。它基于 type hints 推断参数,并建立在 click 之上。

真实项目:CSV 数据处理 CLI

下面是一个完整的、生产风格的 CLI 工具,它读取 CSV 文件、过滤数据、计算聚合,并以多种格式输出结果。这展示了 argparse 各种模式如何在真实项目中组合使用。

#!/usr/bin/env python3
"""csv_processor.py -- A CLI tool for analyzing CSV data."""
 
import argparse
import csv
import json
import sys
from collections import defaultdict
from pathlib import Path
 
 
def read_csv(filepath, encoding="utf-8"):
    """Read a CSV file and return a list of dictionaries."""
    with open(filepath, "r", encoding=encoding) as f:
        reader = csv.DictReader(f)
        return list(reader)
 
 
def cmd_info(args):
    """Display information about a CSV file."""
    rows = read_csv(args.file, args.encoding)
    columns = list(rows[0].keys()) if rows else []
 
    print(f"File:    {args.file}")
    print(f"Rows:    {len(rows)}")
    print(f"Columns: {len(columns)}")
 
    if args.verbose:
        print("\nColumn details:")
        for col in columns:
            non_empty = sum(1 for r in rows if r[col].strip())
            unique = len(set(r[col] for r in rows))
            print(f"  {col:30s}  non-empty: {non_empty:>6d}  unique: {unique:>6d}")
 
 
def cmd_filter(args):
    """Filter rows where a column matches a value."""
    rows = read_csv(args.file, args.encoding)
 
    if args.column not in rows[0]:
        print(f"Error: column '{args.column}' not found.", file=sys.stderr)
        print(f"Available columns: {', '.join(rows[0].keys())}", file=sys.stderr)
        sys.exit(1)
 
    if args.match == "exact":
        filtered = [r for r in rows if r[args.column] == args.value]
    elif args.match == "contains":
        filtered = [r for r in rows if args.value.lower() in r[args.column].lower()]
    elif args.match == "startswith":
        filtered = [r for r in rows if r[args.column].startswith(args.value)]
    elif args.match == "gt":
        threshold = float(args.value)
        filtered = [r for r in rows if _safe_float(r[args.column], float("-inf")) > threshold]
    elif args.match == "lt":
        threshold = float(args.value)
        filtered = [r for r in rows if _safe_float(r[args.column], float("inf")) < threshold]
 
    print(f"Matched {len(filtered)} of {len(rows)} rows", file=sys.stderr)
    _output_rows(filtered, args)
 
 
def cmd_aggregate(args):
    """Group by a column and compute aggregations."""
    rows = read_csv(args.file, args.encoding)
 
    if args.group_by not in rows[0]:
        print(f"Error: column '{args.group_by}' not found.", file=sys.stderr)
        sys.exit(1)
 
    groups = defaultdict(list)
    for row in rows:
        key = row[args.group_by]
        groups[key].append(row)
 
    results = []
    for key, group_rows in sorted(groups.items()):
        result = {args.group_by: key, "count": len(group_rows)}
 
        if args.sum_column:
            total = sum(_safe_float(r[args.sum_column], 0) for r in group_rows)
            result[f"sum_{args.sum_column}"] = round(total, 2)
 
        if args.avg_column:
            values = [_safe_float(r[args.avg_column], None) for r in group_rows]
            values = [v for v in values if v is not None]
            if values:
                result[f"avg_{args.avg_column}"] = round(sum(values) / len(values), 2)
 
        results.append(result)
 
    if args.sort_by == "count":
        results.sort(key=lambda r: r["count"], reverse=True)
 
    if args.top:
        results = results[:args.top]
 
    _output_rows(results, args)
 
 
def cmd_convert(args):
    """Convert CSV to another format."""
    rows = read_csv(args.file, args.encoding)
 
    if args.columns:
        rows = [{k: r.get(k, "") for k in args.columns} for r in rows]
 
    if args.limit:
        rows = rows[:args.limit]
 
    _output_rows(rows, args)
    count = min(args.limit, len(rows)) if args.limit else len(rows)
    print(f"Converted {count} rows", file=sys.stderr)
 
 
def _safe_float(value, default):
    """Try to convert a string to float, return default on failure."""
    try:
        return float(value)
    except (ValueError, TypeError):
        return default
 
 
def _output_rows(rows, args):
    """Write rows to the specified output in the specified format."""
    if not rows:
        return
 
    output_file = open(args.output, "w", newline="") if args.output else sys.stdout
 
    if args.format == "json":
        json.dump(rows, output_file, indent=2)
        output_file.write("\n")
    elif args.format == "csv":
        writer = csv.DictWriter(output_file, fieldnames=rows[0].keys())
        writer.writeheader()
        writer.writerows(rows)
    elif args.format == "tsv":
        writer = csv.DictWriter(output_file, fieldnames=rows[0].keys(),
                                delimiter="\t")
        writer.writeheader()
        writer.writerows(rows)
    elif args.format == "table":
        _print_table(rows, output_file)
 
    if args.output:
        output_file.close()
 
 
def _print_table(rows, out):
    """Print rows as an aligned text table."""
    if not rows:
        return
 
    headers = list(rows[0].keys())
    widths = {h: len(h) for h in headers}
    for row in rows:
        for h in headers:
            widths[h] = max(widths[h], len(str(row.get(h, ""))))
 
    header_line = "  ".join(h.ljust(widths[h]) for h in headers)
    separator = "  ".join("-" * widths[h] for h in headers)
    out.write(header_line + "\n")
    out.write(separator + "\n")
    for row in rows:
        line = "  ".join(str(row.get(h, "")).ljust(widths[h]) for h in headers)
        out.write(line + "\n")
 
 
def main():
    parser = argparse.ArgumentParser(
        prog="csv_processor",
        description="Analyze, filter, aggregate, and convert CSV files.",
        epilog="Examples:\n"
               "  csv_processor info sales.csv --verbose\n"
               "  csv_processor filter sales.csv --column region --value East\n"
               "  csv_processor aggregate sales.csv --group-by region --sum revenue\n"
               "  csv_processor convert sales.csv --format json -o sales.json\n",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--encoding", default="utf-8", help="File encoding")
 
    subparsers = parser.add_subparsers(dest="command", required=True,
                                        help="Available commands")
 
    # --- info ---
    info_p = subparsers.add_parser("info", help="Show CSV file information")
    info_p.add_argument("file", help="CSV file to inspect")
    info_p.add_argument("-v", "--verbose", action="store_true",
                        help="Show per-column statistics")
    info_p.set_defaults(func=cmd_info, format="table", output=None)
 
    # --- filter ---
    filter_p = subparsers.add_parser("filter", help="Filter rows by column value")
    filter_p.add_argument("file", help="Input CSV file")
    filter_p.add_argument("--column", "-c", required=True, help="Column to filter on")
    filter_p.add_argument("--value", "-V", required=True, help="Value to match against")
    filter_p.add_argument("--match", "-m",
                          choices=["exact", "contains", "startswith", "gt", "lt"],
                          default="exact", help="Match mode (default: exact)")
    filter_p.add_argument("--format", "-f",
                          choices=["csv", "json", "tsv", "table"],
                          default="table", help="Output format")
    filter_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    filter_p.set_defaults(func=cmd_filter)
 
    # --- aggregate ---
    agg_p = subparsers.add_parser("aggregate", help="Group and aggregate data")
    agg_p.add_argument("file", help="Input CSV file")
    agg_p.add_argument("--group-by", "-g", required=True, help="Column to group by")
    agg_p.add_argument("--sum", dest="sum_column", help="Column to sum")
    agg_p.add_argument("--avg", dest="avg_column", help="Column to average")
    agg_p.add_argument("--sort-by", choices=["name", "count"], default="name",
                        help="Sort results by name or count")
    agg_p.add_argument("--top", type=int, help="Show only top N groups")
    agg_p.add_argument("--format", "-f",
                        choices=["csv", "json", "tsv", "table"],
                        default="table", help="Output format")
    agg_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    agg_p.set_defaults(func=cmd_aggregate)
 
    # --- convert ---
    conv_p = subparsers.add_parser("convert", help="Convert CSV to another format")
    conv_p.add_argument("file", help="Input CSV file")
    conv_p.add_argument("--format", "-f", required=True,
                        choices=["json", "tsv", "csv", "table"],
                        help="Target format")
    conv_p.add_argument("--columns", nargs="+", metavar="COL",
                        help="Include only these columns")
    conv_p.add_argument("--limit", type=int, help="Max rows to convert")
    conv_p.add_argument("-o", "--output", help="Output file (default: stdout)")
    conv_p.set_defaults(func=cmd_convert)
 
    args = parser.parse_args()
    args.func(args)
 
 
if __name__ == "__main__":
    main()

这个工具结合了子命令、自定义类型处理、多种输出格式、参数组和正确的错误报告。每个子命令都有自己明确的参数集。用户还能为每个子命令获得完整的 --help

可视化结果:在使用类似这样的 CLI 工具过滤或聚合 CSV 数据后,你通常会想把输出进一步可视化。PyGWalker (opens in a new tab) 可以把任何 pandas DataFrame 变成交互式、类似 Tableau 的可视化界面 -- 直接在 Jupyter notebook 或脚本中使用。你可以把 CLI 输出导入 DataFrame,然后使用 PyGWalker 在无需编写绘图代码的情况下构建图表:

import pandas as pd
import pygwalker as pyg
 
# Load the CSV processor output
df = pd.read_csv("aggregated_results.csv")
 
# Launch interactive visual explorer
walker = pyg.walk(df)

在构建这类 CLI 工具时,像 RunCell (opens in a new tab) 这样的交互式开发工具可以让你在 Jupyter 中借助 AI 支持来构建和测试 CLI。你可以在 notebook 单元中原型化参数解析逻辑,使用不同的参数组合进行测试,然后导出为独立脚本。

常见错误与修复

1. error: unrecognized arguments

当你传入了一个未定义的参数时,就会发生这个错误:

$ python tool.py --output results.csv --compress
error: unrecognized arguments: --compress

修复:要么把这个参数添加到 parser 中,要么在你确实想忽略未知参数时使用 parse_known_args()

args, unknown = parser.parse_known_args()
# args contains recognized arguments
# unknown is a list of unrecognized strings

2. error: argument --count: invalid int value

$ python tool.py --count three
error: argument --count: invalid int value: 'three'

修复:这说明 argparse 正常工作。用户给 type=int 的参数传了一个非整数字符串。你的自定义类型函数应该抛出带有清晰消息的 ArgumentTypeError

3. 带短横线的属性名

parser.add_argument("--log-level", default="INFO")
args = parser.parse_args()
 
# This is a syntax error:
# print(args.log-level)  # Python interprets this as args.log minus level
 
# Correct:
print(args.log_level)  # Dashes become underscores

4. 子命令没有触发任何动作

# Problem: no error when no subcommand is given
subparsers = parser.add_subparsers(dest="command")
# Solution: add required=True
subparsers = parser.add_subparsers(dest="command", required=True)

如果没有 required=True,脚本在没有子命令时会静默地什么也不做。对于 Python 3.7+,应始终给 subparsers 设置 required=True

5. 因继承导致的重复帮助信息

如果父解析器和子解析器都定义了相同参数,用户会很困惑:

# BAD: --verbose defined on both parent and child
parser.add_argument("--verbose", action="store_true")
sub = subparsers.add_parser("run")
sub.add_argument("--verbose", action="store_true")  # Shadows parent's --verbose
 
# GOOD: put shared arguments on the parent only
parser.add_argument("--verbose", action="store_true")

6. parse_args() 在导入时执行

# BAD: parse_args() fires when another module imports this file
parser = argparse.ArgumentParser()
parser.add_argument("--name")
args = parser.parse_args()  # Crashes if imported without CLI args
 
# GOOD: wrap everything in main()
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--name")
    args = parser.parse_args()
    return args
 
if __name__ == "__main__":
    main()

7. FileType 过早打开文件

# Risky: file is opened at parse time, before your code validates other args
parser.add_argument("output", type=argparse.FileType("w"))
 
# Safer: accept a path string, open the file yourself with a context manager
parser.add_argument("output", help="Output file path")
args = parser.parse_args()
with open(args.output, "w") as f:
    f.write("data")

高级模式

用父解析器共享参数

当多个子命令共享同一组参数时,使用父解析器可以避免重复代码:

import argparse
 
# Shared arguments
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument("--verbose", "-v", action="store_true")
parent.add_argument("--output", "-o", default="output.csv")
 
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
 
# Both subcommands inherit --verbose and --output
cmd_a = subparsers.add_parser("analyze", parents=[parent])
cmd_a.add_argument("file", help="File to analyze")
 
cmd_b = subparsers.add_parser("compare", parents=[parent])
cmd_b.add_argument("file_a", help="First file")
cmd_b.add_argument("file_b", help="Second file")

环境变量默认值

从环境变量中获取默认值,可以让部署更灵活:

import argparse
import os
 
parser = argparse.ArgumentParser()
parser.add_argument("--api-key",
                    default=os.environ.get("API_KEY"),
                    help="API key (or set API_KEY env var)")
parser.add_argument("--port", type=int,
                    default=int(os.environ.get("PORT", "8080")),
                    help="Server port (default: $PORT or 8080)")
args = parser.parse_args()
 
if not args.api_key:
    parser.error("--api-key is required (or set API_KEY environment variable)")

测试 argparse 脚本

你可以把 argparse 的测试集成到 unittest 或 pytest 测试套件中。通过向 parse_args() 传入参数列表来测试 CLI 工具:

import argparse
 
def create_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument("name")
    parser.add_argument("--count", type=int, default=1)
    return parser
 
# In your test file
def test_parser_defaults():
    parser = create_parser()
    args = parser.parse_args(["Alice"])
    assert args.name == "Alice"
    assert args.count == 1
 
def test_parser_with_options():
    parser = create_parser()
    args = parser.parse_args(["Bob", "--count", "5"])
    assert args.name == "Bob"
    assert args.count == 5

create_parser()main() 分开,可以让你的参数定义更容易测试,而无需运行整个脚本。

FAQ

Python argparse 用来做什么?

Python argparse 是标准库中用于解析命令行参数的模块。它从 sys.argv 中读取字符串,将其转换为带类型的 Python 对象,校验输入,生成 --help 输出,并在用户提供错误参数时报告错误。它用于把脚本变成正规的 CLI 工具,使其能够接收可配置输入,而无需修改源代码。

argparse 中位置参数和可选参数有什么区别?

位置参数的名字中没有短横线(例如 parser.add_argument("filename"))。它们默认是必需的,并按位置匹配。可选参数以短横线开头(例如 parser.add_argument("--verbose"))。它们默认是可选的,并且可以以任意顺序出现。你可以通过 required=True 把可选参数变成必填。

如何创建像 git commit 或 docker build 这样的子命令?

使用 parser.add_subparsers() 创建子命令组。对每个子命令调用 subparsers.add_parser("name")。每个子解析器都有自己的参数集合。使用 set_defaults(func=handler_function) 把每个子命令绑定到处理函数,然后调用 args.func(args) 进行分发。

我该为 Python CLI 使用 argparse 还是 click?

如果你需要零外部依赖,而且 CLI 比较简单,就用 argparse。如果你需要交互式提示、彩色终端输出、进度条或深层嵌套命令组等高级功能,就用 click。Typer 也是一个选项,它使用 Python 类型提示,比 click 需要更少的样板代码。

如何使用 argparse 校验自定义输入类型?

写一个函数,接收字符串并返回转换后的值。如果输入无效,就抛出带描述性信息的 argparse.ArgumentTypeError。然后把这个函数作为 type 参数传入:parser.add_argument("--date", type=my_date_parser)。argparse 会自动调用这个函数,并在校验失败时显示错误信息。

可以在 argparse 中使用环境变量吗?

可以。把 default 参数设置为读取 os.environparser.add_argument("--api-key", default=os.environ.get("API_KEY"))。这样用户既可以通过环境变量配置值,也可以通过命令行参数覆盖它们。

结论

Python argparse 将脚本从脆弱的“改源码才能运行”的工具,变成了正规的命令行程序。这个模块负责类型转换、校验、帮助信息生成和错误消息,因此你可以专注于脚本要做什么,而不是它如何接收输入。

需要记住的模式:

  • 需要必填输入时使用位置参数,需要可选输入时使用带短横线的参数。
  • 使用 type 做自动转换,choices 限制取值,nargs 接收多个值。
  • 布尔标志使用 action="store_true",verbosity 等级使用 action="count"
  • add_subparsers()set_defaults(func=handler) 这种分发模式构建复杂工具。
  • 当参数互相冲突时,使用互斥组。
  • 把解析逻辑放进 main(),并用 if __name__ == "__main__" 保护它。
  • 为领域特定的校验编写自定义类型函数。

argparse 不是 Python 中最新的参数解析库。但它随每个 Python 安装一起提供,能够满足绝大多数 CLI 需求,并且能产出符合用户预期的工具。对大多数 Python 开发者来说,这就是正确选择。当你的 CLI 工具需要调用外部命令时,可以把 argparse 和 subprocess 模块结合起来,形成完整的命令行自动化方案。

相关文章

📚