Python Argparse: コマンドラインインターフェースを正しく作る
更新日
Python スクリプトを書いて、必要なとおりにデータを処理できるようになったとします。そこへ同僚が「それを使いたい」と言ってきます。「14行目のファイル名と37行目のしきい値を変えるだけだよ」とあなたは伝えます。すると相手は違う行を変えてしまいます。スクリプトは壊れます。あなたは、動いていたコードに対する他人の修正をデバッグするために 20 分を費やします。誰かがパラメータを少し調整したいだけなのに、このことが毎回起こり、スクリプトが大きくなるほど状況は悪化します。スクリプト内にハードコードされた値は、チームでも、自動化でも、本番運用でもスケールしません。
Python の argparse モジュールは、どんなスクリプトでも本格的なコマンドラインツールに変えてくれます。ユーザーは実行時に引数を渡します。モジュールはパース、型変換、検証、ヘルプメッセージ生成を担当します。標準ライブラリに含まれているので、追加インストールは不要です。このガイドでは、基本の位置引数からサブコマンド、実践的な CLI 設計パターンまで、必要な argparse の機能をすべて解説します。
argparse の役割と sys.argv より優れている理由
すべての Python スクリプトは、コマンドラインからの文字列リストである sys.argv にアクセスできます。これを手動で解析することも可能です。
import sys
filename = sys.argv[1]
threshold = float(sys.argv[2])
verbose = "--verbose" in sys.argvこれは使い捨てスクリプトなら動きますが、すぐに限界が来ます。ヘルプメッセージはありません。型検証もありません。ユーザーが引数を忘れたときのエラーメッセージもありません。オプションフラグを正しく扱う方法もありません。パラメータを 1 つ追加・削除するだけでインデックスが壊れます。
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は自動的に float になります。--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このスクリプトについて、3 つの点に注目してください。
- パース処理はモジュール直下ではなく
main()の中にある ため、インポートしても引数パースが走りません。 if __name__ == "__main__"ガード により、main()はスクリプトを直接実行したときだけ動き、インポート時には動きません。- 短いフラグと長いフラグ (
-cと--count) により、ユーザーは簡潔さと分かりやすさを選べます。
この 3 つのパターンは、よく書かれた 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.csvnargs で複数の値を受け取る
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 アクション
ツールによっては、詳細度を表すためにフラグを複数回指定します(-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']型と choices
組み込みの型変換
type パラメータは、文字列を受け取って値を返す任意の callable を受け付けます。組み込み型や、ファイルパス用の 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 の中でも特に強力な機能の 1 つです。パース時に入力を検証し、変換できます。
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 は裸のパスより分かりやすい)や、多数の必須パラメータがあり、順序ミスを防ぎたい場合です。
本当に必須の入力には、位置引数のほうがより自然なことが多いです。
# 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 ツールで使われる標準的な方法です。
ヘルプを見やすくする Argument Group
ツールに多くの引数があるときは、論理的な見出しでグループ分けできます。
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
usage メッセージに表示される名前を制御できます。
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 TOP ではなく --top N が表示され、help テキストには実際のデフォルト値が表示されます。
argparse と sys.argv / click / 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 -- 1 つの値を拾うだけの使い捨てスクリプトで、検証を気にしない場合。
- argparse -- 依存関係を増やせないが、本格的な CLI が必要な場合。多くのプロジェクトでの標準的な選択です。
- click -- ネストされたコマンドグループ、対話型プロンプト、カラー出力、プラグインシステムなどが必要な複雑な CLI を作る場合。
- typer -- Python 3.7+ で、少ないボイラープレートで書きたい場合。
[type hints](/topics/Python/python-type-hints)を使い、click をベースにしています。
実践例: CSV データ処理 CLI
ここでは、CSV ファイルの読み込み、フィルタリング、集計、複数形式への出力を行う、本番向けの CLI ツールを示します。実際のプロジェクトで 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 を、Jupyter notebook やスクリプト内でそのまま操作できる Tableau 風の対話型可視化インターフェースに変えてくれます。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) を使うと、AI の支援付きで Jupyter 内で CLI ツールを構築・テストできます。ノートブックのセルで引数パースのロジックを試し、さまざまな引数の組み合わせをテストしてから、スタンドアロンのスクリプトとして書き出せます。
よくあるエラーと対処法
1. error: unrecognized arguments
これは、定義していない引数を渡したときに起こります。
$ python tool.py --output results.csv --compress
error: unrecognized arguments: --compress対処法: その引数をパーサーに追加するか、未知の引数を意図的に無視したい場合は 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. import 時に 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 == 5create_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 も選択肢の 1 つで、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"、詳細レベルには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 ベースのツールをテストする