Python Argparse: CLI-Tools richtig bauen
Aktualisiert am
Du hast ein Python-Skript geschrieben, das Daten genau so verarbeitet, wie du es brauchst. Dann fragt ein Kollege, ob er es ebenfalls verwenden kann. „Ändere einfach den Dateinamen in Zeile 14 und den Schwellenwert in Zeile 37“, sagst du. Er ändert die falsche Zeile. Das Skript bricht ab. Du verbringst 20 Minuten damit, den Edit eines anderen an deinem funktionierenden Code zu debuggen. Das passiert jedes Mal, wenn jemand einen Parameter anpassen muss, und es wird schlimmer, je größer das Skript wird. Hardcodierte Werte in Skripten skalieren nicht -- weder für Teams noch für Automatisierung oder Produktion.
Das argparse-Modul von Python behebt dieses Problem, indem es jedes Skript in ein echtes Befehlszeilentool verwandelt. Benutzer übergeben Argumente, wenn sie das Skript ausführen. Das Modul übernimmt das Parsen, die Typkonvertierung, die Validierung und die Erstellung von Hilfetexten. Es ist Teil der Standardbibliothek, es muss also nichts installiert werden. Dieser Leitfaden führt dich durch alle argparse-Funktionen, die du brauchst -- von einfachen Positionsargumenten bis hin zu Subcommands und realen CLI-Designmustern.
Was argparse macht und warum es besser ist als sys.argv
Jedes Python-Skript hat Zugriff auf sys.argv, eine rohe Liste von Strings aus der Befehlszeile. Du kannst sie manuell parsen:
import sys
filename = sys.argv[1]
threshold = float(sys.argv[2])
verbose = "--verbose" in sys.argvDas funktioniert für Wegwerfskripte, bricht aber schnell zusammen. Es gibt keine Hilfetexte. Keine Typvalidierung. Keine Fehlermeldungen, wenn Nutzer ein Argument vergessen. Keine Möglichkeit, optionale Flags sauber zu behandeln. Die Indizierung geht in dem Moment kaputt, in dem du einen Parameter hinzufügst oder entfernst.
argparse löst all diese Probleme mit einer deklarativen API. Du definierst, welche Argumente dein Skript akzeptiert, und argparse erledigt den Rest:
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}")Was du kostenlos bekommst:
- Automatisches
--help: Führepython script.py --helpaus und Benutzer sehen jedes Argument, seinen Typ und seinen Standardwert. - Typkonvertierung:
--threshold 0.8wird automatisch zu einem Float. Wenn jemand--threshold abcangibt, gibt argparse einen klaren Fehler aus. - Validierung: Fehlende erforderliche Argumente erzeugen hilfreiche Fehlermeldungen statt kryptischer
IndexError-Tracebacks. - Flexible Reihenfolge: Optionale Argumente können in beliebiger Reihenfolge erscheinen.
--verbose --threshold 0.8funktioniert genauso wie--threshold 0.8 --verbose.
So sieht die Hilfeausgabe aus:
$ 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 outputDein erstes CLI-Skript
Lass uns ein vollständiges, funktionierendes Skript von Grund auf erstellen. Dieses Tool begrüßt eine Person mit ihrem Namen und kann optional angeben, wie oft die Begrüßung wiederholt werden soll.
#!/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()Teste es im Terminal:
$ 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: nameAchte auf drei Dinge in diesem Skript:
- Die Parsing-Logik lebt in
main(), nicht auf Modulebene. Dadurch bleibt das Skript importierbar, ohne sofort das Parsen auszulösen. - Der Schutz
if __name__ == "__main__"stellt sicher, dassmain()nur läuft, wenn das Skript direkt ausgeführt wird, nicht wenn es importiert wird. - Kurze und lange Flags (
-cund--count) geben Benutzern die Wahl zwischen Kürze und Klarheit.
Diese drei Muster tauchen in jedem sauber geschriebenen argparse-Skript auf. Verwende sie von Anfang an.
Positionsargumente
Positionsargumente werden ohne Bindestriche definiert. Sie sind standardmäßig erforderlich und werden anhand ihrer Position in der Befehlszeile zugeordnet.
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.csvMehrere Positionswerte mit nargs
Der Parameter nargs steuert, wie viele Werte ein Argument konsumiert:
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 500Hier ist die vollständige nargs-Referenz:
| 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 |
Typkonvertierung bei Positionsargumenten
Positionsargumente sind standardmäßig Strings. Füge type hinzu, um sie zu konvertieren:
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'Die Fehlermeldung wird automatisch erzeugt und sagt dem Benutzer genau, was schiefgelaufen ist.
Optionale Argumente
Optionale Argumente beginnen mit - (Kurzform) oder -- (Langform). Sie sind standardmäßig optional und können in beliebiger Reihenfolge erscheinen.
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: TrueStandardwerte
Jedes optionale Argument hat einen Standardwert. Wenn du keinen explizit setzt, ist er 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}")Hinweis: Wenn ein Argumentname Bindestriche verwendet (--log-file), wandelt argparse sie für den Attributnamen in Unterstriche um: args.log_file.
Die Actions store_true und store_false
Boolesche Flags nehmen keinen Wert an. Sie sind entweder vorhanden oder nicht vorhanden:
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 FalseDie count-Action für Verbosity-Stufen
Manche Tools verwenden wiederholte Flags für unterschiedliche Ausführlichkeitsstufen (-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 everythingDie append-Action für wiederholte Argumente
Verwende action="append", wenn Benutzer dasselbe Flag mehrmals angeben sollen:
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']Typ und Choices
Eingebaute Typkonvertierung
Der Parameter type akzeptiert jede aufrufbare Funktion, die einen String entgegennimmt und einen Wert zurückgibt. Du kannst eingebaute Typen, pathlib.Path für Dateipfade oder eigene Funktionen verwenden:
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()Werte mit choices einschränken
Verwende choices, um den Wert eines Arguments auf bestimmte erlaubte Optionen zu begrenzen:
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')Eigene Type-Funktionen
Eigene Type-Funktionen gehören zu den leistungsstärksten Features von argparse. Sie erlauben es dir, Eingaben bereits beim Parsen zu validieren und umzuwandeln:
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)Jede Validierungsfehlermeldung erzeugt eine klare, umsetzbare Nachricht, ohne dass du in deinem Hauptcode if/else-Logik schreiben musst.
Erforderliche optionale Argumente
Du kannst ein optionales Argument als erforderlich erzwingen:
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, --outputWarum das manchmal ein Code-Smell ist: Wenn jedes „optionale“ Argument tatsächlich erforderlich ist, solltest du wahrscheinlich Positionsargumente verwenden. Erforderliche optionale Argumente sind sinnvoll, wenn der Flag-Name Klarheit schafft (--config ist klarer als ein bloßer Pfad) oder wenn du viele erforderliche Parameter hast und benannte Flags Verwechslungsfehler bei der Reihenfolge verhindern.
Ein besseres Muster für wirklich zwingende Eingaben sind oft Positionsargumente:
# 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")Gegenseitig ausschließende Gruppen
Manchmal widersprechen sich Argumente gegenseitig. Ein Skript könnte entweder JSON- oder CSV-Ausgabe erzeugen, aber nicht beides gleichzeitig. add_mutually_exclusive_group() erzwingt diese Einschränkung:
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 ParquetEine praktische Alternative zur Formatauswahl verwendet choices statt separater Flags:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--format", choices=["json", "csv", "parquet"],
required=True, help="Output format")
args = parser.parse_args()Beide Ansätze funktionieren. Gegenseitig ausschließende Gruppen sind besser, wenn jede Option zusätzliche eigene Argumente benötigt. Der choices-Ansatz ist besser, wenn die Optionen einfache Strings sind.
Subcommands mit add_subparsers
Echte CLI-Tools verwenden Subcommands. Denk an git commit, docker build, pip install. Jeder Subcommand hat eigene Argumente, eigenen Hilfetext und eine eigene Handler-Funktion. argparse unterstützt dieses Muster mit 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 deployingDas Schlüsselpattern hier ist set_defaults(func=handler). Jeder Subcommand speichert seine Handler-Funktion im geparsten Namespace, und die Hauptfunktion ruft sie mit args.func(args) auf. Das ist der Standardansatz in produktiven CLI-Tools.
Argumentgruppen für bessere Hilfe
Wenn dein Tool viele Argumente hat, kannst du sie unter logischen Überschriften gruppieren:
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()Die --help-Ausgabe gruppiert die Argumente unter diesen Überschriften, wodurch lange Argumentlisten leichter lesbar werden. Das ist rein eine Darstellungsfunktion -- sie ändert nicht, wie die Argumente geparst werden.
Benutzerdefinierte Hilfe und Formatierung
Der Parameter 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 und %(default)s
Steuere, was in den Usage-Meldungen erscheint:
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)")Dadurch erscheint in der Usage-Ausgabe --top N statt --top TOP, und der Hilfetext zeigt den tatsächlichen Standardwert.
argparse vs sys.argv vs click vs typer
Python bietet mehrere Ansätze zum Parsen von Befehlszeilenargumenten. So unterscheiden sie sich:
| 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+ |
Hier ist dasselbe Tool in jeder der Varianten implementiert:
# --- 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)Wann du welches verwenden solltest:
- sys.argv -- Nur für Einmal-Skripte, bei denen du einen einzelnen Wert abgreifst und keine Validierung brauchst.
- argparse -- Wenn du ein echtes CLI brauchst, aber keine externen Abhängigkeiten hinzufügen kannst. Das ist für die meisten Projekte die richtige Standardwahl.
- click -- Wenn du ein komplexes CLI mit verschachtelten Befehlsebenen, interaktiven Eingaben, farbiger Ausgabe und Plugin-Systemen baust.
- typer -- Wenn du möglichst wenig Boilerplate willst und Python 3.7+ verwendest. Es leitet Argumente aus type hints ab und baut auf click auf.
Praxisprojekt: CLI für einen CSV-Datenprozessor
Hier ist ein vollständiges, produktionsnahes CLI-Tool, das CSV-Dateien liest, Daten filtert, Aggregationen berechnet und Ergebnisse in mehrere Formate ausgibt. Es zeigt, wie argparse-Muster in einem realen Projekt zusammenwirken.
#!/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()Dieses Tool kombiniert Subcommands, benutzerdefiniertes Typverhalten, mehrere Ausgabeformate, Argumentgruppen und saubere Fehlermeldungen. Jeder Subcommand hat seinen eigenen, fokussierten Argumentsatz. Benutzer bekommen für jeden Subcommand vollständiges --help.
Ergebnisse visuell untersuchen: Nachdem du CSV-Daten mit einem CLI-Tool wie diesem gefiltert oder aggregiert hast, möchtest du die Ausgabe oft visuell untersuchen. PyGWalker (opens in a new tab) verwandelt jedes pandas DataFrame in eine interaktive, Tableau-ähnliche Visualisierungsoberfläche -- direkt in einem Jupyter-Notebook oder Skript. Leite die Ausgabe deines CLI-Tools in ein DataFrame und verwende PyGWalker, um Diagramme ohne eigenen Plot-Code zu erstellen:
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)Für interaktive Entwicklung von CLI-Tools wie diesem ermöglicht dir RunCell (opens in a new tab) das Erstellen und Testen von CLI-Tools direkt in Jupyter mit KI-Unterstützung. Du kannst Argument-Parsing-Logik in Notebook-Zellen prototypisieren, verschiedene Argumentkombinationen testen und anschließend in ein eigenständiges Skript exportieren.
Häufige Fehler und Lösungen
1. error: unrecognized arguments
Das passiert, wenn du ein Argument übergibst, das nicht definiert wurde:
$ python tool.py --output results.csv --compress
error: unrecognized arguments: --compressFix: Füge das Argument entweder zu deinem Parser hinzu oder verwende parse_known_args(), wenn du unbekannte Argumente absichtlich ignorieren möchtest:
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'Fix: Das ist argparse, das korrekt arbeitet. Der Benutzer hat einen nicht numerischen String an ein type=int-Argument übergeben. Deine eigenen Typfunktionen sollten ArgumentTypeError mit einer klaren Meldung auslösen.
3. Attributnamen mit Bindestrichen
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. Subcommand löst keine Aktion aus
# 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)Ohne required=True passiert beim Ausführen des Skripts ohne Subcommand stillschweigend nichts. In Python 3.7+ solltest du bei Subparsers immer required=True setzen.
5. Doppelte Hilfetexte durch Vererbung
Wenn sowohl ein Parent-Parser als auch ein Subparser dasselbe Argument definieren, wird es für Benutzer verwirrend:
# 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() läuft beim Import
# 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 öffnet Dateien zu früh
# 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")Erweiterte Muster
Parent-Parser für gemeinsam genutzte Argumente
Wenn mehrere Subcommands denselben Argumentsatz teilen, verwende Parent-Parser, um Wiederholungen zu vermeiden:
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")Standardwerte aus Umgebungsvariablen
Ziehe Standardwerte aus Umgebungsvariablen, um mehr Flexibilität bei Deployments zu erhalten:
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-Skripte testen
Du kannst argparse-Tests in deine unittest- oder pytest-Testsuiten integrieren. Teste deine CLI-Tools, indem du Argumentlisten an parse_args() übergibst:
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 == 5Wenn du create_parser() von main() trennst, werden deine Argumentdefinitionen testbar, ohne das ganze Skript auszuführen.
FAQ
Wofür wird Python argparse verwendet?
Python argparse ist das Modul der Standardbibliothek zum Parsen von Befehlszeilenargumenten. Es liest Strings aus sys.argv, wandelt sie in typisierte Python-Objekte um, validiert Eingaben, erzeugt --help-Ausgaben und meldet Fehler, wenn Benutzer falsche Argumente übergeben. Es wird verwendet, um Skripte in echte CLI-Tools zu verwandeln, die konfigurierbare Eingaben akzeptieren, ohne den Quellcode zu bearbeiten.
Was ist der Unterschied zwischen Positions- und optionalen Argumenten in argparse?
Positionsargumente haben keine Bindestriche in ihrem Namen (z. B. parser.add_argument("filename")). Sie sind standardmäßig erforderlich und werden nach Position zugeordnet. Optionale Argumente beginnen mit Bindestrichen (z. B. parser.add_argument("--verbose")). Sie sind standardmäßig optional und können in beliebiger Reihenfolge erscheinen. Du kannst optionale Argumente mit required=True verpflichtend machen.
Wie erstelle ich Subcommands wie git commit oder docker build?
Verwende parser.add_subparsers(), um eine Subcommand-Gruppe zu erstellen. Rufe für jeden Subcommand subparsers.add_parser("name") auf. Jeder Subparser bekommt seinen eigenen Argumentsatz. Verwende set_defaults(func=handler_function), um jeden Subcommand mit einem Handler zu verknüpfen, und rufe dann args.func(args) auf, um zu dispatchen.
Sollte ich argparse oder click für meine Python-CLI verwenden?
Verwende argparse, wenn du keine externen Abhängigkeiten willst und dein CLI schlicht ist. Verwende click, wenn du erweiterte Funktionen wie interaktive Eingaben, farbige Terminalausgabe, Fortschrittsanzeigen oder tief verschachtelte Befehlsgruppen brauchst. Typer ist eine weitere Option, die Python-Type-Hints verwendet und noch weniger Boilerplate als click benötigt.
Wie validiere ich eigene Eingabetypen mit argparse?
Schreibe eine Funktion, die einen String entgegennimmt und den konvertierten Wert zurückgibt. Wenn die Eingabe ungültig ist, wirf argparse.ArgumentTypeError mit einer aussagekräftigen Meldung. Übergib diese Funktion als type-Parameter: parser.add_argument("--date", type=my_date_parser). argparse ruft deine Funktion automatisch auf und zeigt die Fehlermeldung an, wenn die Validierung fehlschlägt.
Kann ich argparse mit Umgebungsvariablen verwenden?
Ja. Setze den default-Parameter so, dass er aus os.environ liest: parser.add_argument("--api-key", default=os.environ.get("API_KEY")). So können Benutzer Werte über Umgebungsvariablen konfigurieren und trotzdem Befehlszeilen-Flags zur Überschreibung nutzen.
Fazit
Python argparse verwandelt Skripte von fragilen „Ändere-den-Quellcode“-Werkzeugen in echte Befehlszeilenprogramme. Das Modul übernimmt Typkonvertierung, Validierung, Hilfeerstellung und Fehlermeldungen, sodass du dich auf das konzentrieren kannst, was das Skript tut, nicht darauf, wie es Eingaben erhält.
Die Muster, die du dir merken solltest:
- Verwende Positionsargumente für erforderliche Eingaben, mit Bindestrich versehene Argumente für optionale.
- Setze
typefür automatische Konvertierung,choicesfür eingeschränkte Werte,nargsfür mehrere Werte. - Verwende
action="store_true"für boolesche Flags undaction="count"für Ausführlichkeitsstufen. - Baue komplexe Tools mit
add_subparsers()und dem Dispatch-Musterset_defaults(func=handler). - Verwende gegenseitig ausschließende Gruppen, wenn Argumente sich widersprechen.
- Kapsle das Parsen in
main()und sichere es mitif __name__ == "__main__". - Schreibe eigene Type-Funktionen für domänenspezifische Validierung.
argparse ist nicht die neueste Argument-Parsing-Bibliothek in Python. Aber es ist in jeder Python-Installation enthalten, deckt den Großteil aller CLI-Anforderungen ab und erzeugt Werkzeuge, die sich so verhalten, wie Benutzer es erwarten. Für die meisten Python-Entwickler ist das die richtige Wahl. Wenn dein CLI-Tool externe Befehle aufrufen muss, kombiniere argparse mit dem subprocess-Modul für eine vollständige Automatisierungslösung per Befehlszeile.
Verwandte Anleitungen
- Python subprocess -- Externe Befehle aus deinen CLI-Tools ausführen
- Python pathlib -- Moderne Dateipfadverarbeitung für CLI-Argumente
- Python type hints -- Typannotationen zu deinen CLI-Funktionen hinzufügen
- Python unittest -- Tests für deine auf argparse basierenden Tools schreiben