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注意这个脚本有三点:
- 解析逻辑放在
main()里,而不是模块顶层。这样脚本可以被导入,而不会触发参数解析。 if __name__ == "__main__"保护 确保main()只在脚本直接执行时运行,而不是被导入时运行。- 短参数和长参数(
-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 value | Meaning | Result type | Example |
|---|---|---|---|
| (omitted) | Exactly one value | Single value | "input.csv" |
N (integer) | Exactly N values | List of N items | [10, 100] |
"?" | Zero or one value | Single value or default | "config.yml" or None |
"*" | Zero or more values | List (possibly empty) | [] or ["a", "b"] |
"+" | One or more values | List (error if empty) | ["a"] or ["a", "b"] |
argparse.REMAINDER | All remaining args | List | Everything 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 commit、docker build、pip 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,
)| Formatter | What it does |
|---|---|
HelpFormatter | Default. Wraps text to fit terminal width. |
RawDescriptionHelpFormatter | Preserves newlines in description and epilog. |
RawTextHelpFormatter | Preserves newlines everywhere, including argument help. |
ArgumentDefaultsHelpFormatter | Appends (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 提供了多种命令行参数解析方式。下面是它们的对比:
| Feature | sys.argv | argparse | click | typer |
|---|---|---|---|---|
| Part of stdlib | Yes | Yes | No (pip install) | No (pip install) |
| Type conversion | Manual | Built-in | Built-in | Automatic (type hints) |
| Help generation | None | Automatic | Automatic | Automatic |
| Subcommands | Manual | add_subparsers() | @group.command() | app.command() |
| Validation | Manual | choices, custom type | click.Choice, callbacks | Validators |
| Boilerplate | High | Medium | Low | Very low |
| API style | Imperative | Imperative | Decorators | Decorators + type hints |
| Tab completion | None | None | Via plugin | Built-in |
| Prompting / colors | Manual | None | Built-in | Built-in |
| Testing | Manual | parse_args([...]) | CliRunner | CliRunner |
| Best for | Throwaway scripts | Stdlib-only projects | Complex CLIs | Modern 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 strings2. 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 underscores4. 子命令没有触发任何动作
# 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.environ:parser.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 模块结合起来,形成完整的命令行自动化方案。
相关文章
- Python subprocess -- 从你的 CLI 工具中运行外部命令
- Python pathlib -- 为 CLI 参数提供现代化文件路径处理
- Python type hints -- 为你的 CLI 函数添加类型注解
- Python unittest -- 为基于 argparse 的工具编写测试