Skip to content
Themen
Python
Python Argparse: CLI-Tools richtig bauen

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.argv

Das 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ühre python script.py --help aus und Benutzer sehen jedes Argument, seinen Typ und seinen Standardwert.
  • Typkonvertierung: --threshold 0.8 wird automatisch zu einem Float. Wenn jemand --threshold abc angibt, 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.8 funktioniert 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 output

Dein 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: name

Achte auf drei Dinge in diesem Skript:

  1. Die Parsing-Logik lebt in main(), nicht auf Modulebene. Dadurch bleibt das Skript importierbar, ohne sofort das Parsen auszulösen.
  2. Der Schutz if __name__ == "__main__" stellt sicher, dass main() nur läuft, wenn das Skript direkt ausgeführt wird, nicht wenn es importiert wird.
  3. Kurze und lange Flags (-c und --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.csv

Mehrere 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 500

Hier ist die vollständige nargs-Referenz:

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

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: True

Standardwerte

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 False

Die 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 everything

Die 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, --output

Warum 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 Parquet

Eine 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 deploying

Das 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,
)
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 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:

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+

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: --compress

Fix: 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 strings

2. 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 underscores

4. 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 == 5

Wenn 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 type für automatische Konvertierung, choices für eingeschränkte Werte, nargs für mehrere Werte.
  • Verwende action="store_true" für boolesche Flags und action="count" für Ausführlichkeitsstufen.
  • Baue komplexe Tools mit add_subparsers() und dem Dispatch-Muster set_defaults(func=handler).
  • Verwende gegenseitig ausschließende Gruppen, wenn Argumente sich widersprechen.
  • Kapsle das Parsen in main() und sichere es mit if __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

📚