Skip to content
トピック
Python
Python Argparse: コマンドラインインターフェースを正しく作る

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 つの点に注目してください。

  1. パース処理はモジュール直下ではなく main() の中にある ため、インポートしても引数パースが走りません。
  2. if __name__ == "__main__" ガード により、main() はスクリプトを直接実行したときだけ動き、インポート時には動きません。
  3. 短いフラグと長いフラグ (-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.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 アクション

ツールによっては、詳細度を表すためにフラグを複数回指定します(-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 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 ツールで使われる標準的な方法です。

ヘルプを見やすくする 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,
)
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

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 にはコマンドライン引数を扱う方法がいくつかあります。比較すると次のようになります。

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 -- 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 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. 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 スクリプトのテスト

argparseunittest や 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 も選択肢の 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 モジュールと組み合わせることで、完全なコマンドライン自動化ソリューションになります。

関連ガイド

📚