cat posts/vom-skript-zum-cli.md
Vom Skript zum echten CLI: der Dungeon mit argparse und Typer
Ein gutes CLI nimmt Argumente, Optionen und Unterbefehle entgegen. Mit argparse geht das ohne Abhängigkeiten, mit Typer modern über Type Hints.
Der Dungeon aus der Python-Serie wird bisher wie ein normales Skript gestartet:
python spiel.py
Danach fragt das Programm interaktiv:
Wie heißt Du, Abenteurer?
Wie viele Goldmünzen bringst Du mit?
Für ein Spiel ist das völlig in Ordnung. Bei einem Kommandozeilen-Tool möchtest Du viele Angaben aber schon beim Start mitgeben:
python spiel.py --name Karl --gold 50
Oder Du möchtest unterschiedliche Aktionen anbieten:
python spiel.py neu --name Karl
python spiel.py laden spielstand.json
python spiel.py info spielstand.json
Damit wird aus einem Skript langsam ein CLI – ein Command Line Interface.
Ein gutes CLI nimmt Argumente entgegen, prüft sie, erzeugt Hilfe, liefert brauchbare Fehlermeldungen und signalisiert anderen Programmen über Exit-Codes, ob der Aufruf erfolgreich war.
Python bringt dafür mit argparse bereits ein Modul in der Standardbibliothek
mit. Wer ein komfortableres, stärker auf Type Hints ausgerichtetes API möchte,
kann beispielsweise Typer verwenden.
Warum nicht einfach input(...)?
input(...) ist für interaktive Programme gedacht:
name = input("Wie heißt Du? ")
Ein Mensch sitzt vor dem Terminal, liest die Frage und antwortet.
Ein CLI soll dagegen häufig auch aus einem Shell-Skript, Cronjob oder einer anderen Anwendung aufgerufen werden können:
dungeon neu --name Karl --gold 50
Alle benötigten Informationen stehen bereits im Kommando.
Das lässt sich automatisieren:
dungeon info spielstand.json > status.txt
Ein unerwartetes:
input("Name: ")
würde dagegen mitten im automatisierten Ablauf auf Tastatureingabe warten.
Das heißt nicht, dass interaktive Eingaben in einem CLI grundsätzlich verboten
wären. Ein Spiel kann nach dem Start weiterhin seinen Game-Loop mit input(...)
betreiben.
Startoptionen, Dateipfade und verschiedene Betriebsmodi gehören aber meistens besser auf die Kommandozeile.
Argumente, Optionen und Subcommands
Auf der Kommandozeile begegnen Dir hauptsächlich drei Formen.
Ein Argument ist positionsabhängig:
dungeon laden spielstand.json
spielstand.json steht an einer bestimmten Position und ist hier das Argument
des Befehls laden.
Eine Option beginnt normalerweise mit - oder --:
dungeon neu --name Karl --gold 50
Optionen können Werte entgegennehmen.
Ein Flag ist eine Option, die allein durch ihre Anwesenheit etwas einschaltet:
dungeon neu --schwer
Und ein Subcommand bezeichnet eine Aktion:
dungeon neu
dungeon laden
dungeon info
Die genaue Terminologie unterscheidet sich zwischen Tools gelegentlich etwas. Für unseren Artikel reicht dieses Modell:
dungeon neu --name Karl --schwer
│ │ │
│ │ └── Flag
│ └───────── Option mit Wert
└──────────────── Subcommand
sys.argv: was Python tatsächlich bekommt
Die ursprünglichen Kommandozeilenargumente stellt Python über sys.argv
bereit:
import sys
print(sys.argv)
Startest Du:
python beispiel.py --name Karl
bekommst Du ungefähr:
['beispiel.py', '--name', 'Karl']
Abgesehen vom ersten Eintrag sind das zunächst nur Strings.
Theoretisch könntest Du die Liste selbst auseinandernehmen:
if "--name" in sys.argv:
position = sys.argv.index("--name")
name = sys.argv[position + 1]
Spätestens bei mehreren Optionen wird daraus aber unnötig viel eigener Parser-Code.
Du müsstest selbst klären:
Welche Argumente sind Pflicht?
Welche Optionen dürfen mehrfach vorkommen?
Was bedeutet --help?
Was passiert bei --gold viel?
Welche Fehlermeldung erscheint?
Was ist der Exit-Code?
Welche Optionen gehören zu welchem Subcommand?
Genau dafür gibt es CLI-Parser.
argparse: ohne zusätzliche Dependency
argparse gehört zur Python-Standardbibliothek.
Du musst nichts installieren:
import argparse
Ein erstes CLI:
# dungeon_cli.py
import argparse
def main():
parser = argparse.ArgumentParser(
description="Starte den Dungeon.",
)
parser.add_argument(
"--name",
default="Abenteurer",
help="Name des Helden",
)
parser.add_argument(
"--gold",
type=int,
default=0,
help="Startgold des Helden",
)
parser.add_argument(
"--laden",
help="Pfad zu einem Spielstand",
)
parser.add_argument(
"--schwer",
action="store_true",
help="Startet im schweren Modus",
)
args = parser.parse_args()
print(f"Name: {args.name}")
print(f"Gold: {args.gold}")
if args.laden is not None:
print(f"Lade Spielstand aus {args.laden}")
if args.schwer:
print("Schwerer Modus aktiv.")
if __name__ == "__main__":
main()
Aufruf:
python dungeon_cli.py --name Karl --gold 50 --schwer
Ausgabe:
Name: Karl
Gold: 50
Schwerer Modus aktiv.
argparse übernimmt bereits eine Menge Arbeit, die sonst in unserem Code
landen würde.
Automatische Hilfe
Schon ohne weiteren Code funktioniert:
python dungeon_cli.py --help
Die Hilfe enthält unter anderem die Beschreibung und alle Argumente:
usage: dungeon_cli.py [-h] [--name NAME] [--gold GOLD] [--laden LADEN] [--schwer]
Starte den Dungeon.
options:
-h, --help show this help message and exit
--name NAME Name des Helden
--gold GOLD Startgold des Helden
--laden LADEN Pfad zu einem Spielstand
--schwer Startet im schweren Modus
Die genaue Darstellung hängt von der Python-Version und vom Terminal ab.
Seit Python 3.14 kann argparse Help-Ausgaben standardmäßig auch farbig
darstellen. In einem normalen Terminal sieht das angenehmer aus; in den
Textbeispielen dieses Artikels lasse ich die ANSI-Farbcodes natürlich weg.
Wenn Du Farben ausdrücklich abschalten möchtest:
parser = argparse.ArgumentParser(
description="Starte den Dungeon.",
color=False,
)
color gibt es ab Python 3.14.
Optionen mit Werten
Diese Definition:
parser.add_argument(
"--name",
default="Abenteurer",
help="Name des Helden",
)
erlaubt:
python dungeon_cli.py --name Karl
Danach enthält:
args.name
den String:
"Karl"
Fehlt die Option, wird der Default verwendet:
"Abenteurer"
Typen mit type=...
Die Kommandozeile liefert zunächst Text.
Bei:
python dungeon_cli.py --gold 50
steht dort ursprünglich die Zeichenfolge:
50
Mit:
parser.add_argument(
"--gold",
type=int,
default=0,
)
lässt argparse den Wert durch int() laufen.
Danach ist:
args.gold
tatsächlich:
50
und nicht:
"50"
Ungültiger Input:
python dungeon_cli.py --gold viel
wird automatisch abgefangen.
argparse zeigt eine Fehlermeldung und beendet den Aufruf normalerweise mit
Exit-Code 2.
type für einfache Konvertierungen verwenden
type= eignet sich am besten für überschaubare Umwandlungen wie:
type=int
type=float
type=Path
Komplexe fachliche Validierung würde ich nicht in eine immer ausgefeiltere Converter-Funktion packen.
Die Kommandozeilenschicht soll beispielsweise problemlos aus:
"50"
einen Integer machen.
Ob ein Spieler laut Game Rules höchstens 1000 Gold besitzen darf, gehört dagegen eher zur Anwendungslogik.
Boolean-Flags richtig definieren
Ein Flag wie:
--schwer
hat keinen zusätzlichen Wert.
Mit argparse:
parser.add_argument(
"--schwer",
action="store_true",
help="Startet im schweren Modus",
)
Ohne Flag:
args.schwer
ist:
False
Mit:
python dungeon_cli.py --schwer
wird es:
True
Das hier solltest Du dagegen normalerweise nicht verwenden:
parser.add_argument(
"--schwer",
type=bool,
)
Denn:
bool("False")
ergibt:
True
Jeder nicht leere String ist in Python truthy.
Positive und negative Boolean-Optionen
Manchmal möchtest Du ausdrücklich beide Varianten anbieten:
--farbe
--no-farbe
Dafür gibt es seit Python 3.9:
parser.add_argument(
"--farbe",
action=argparse.BooleanOptionalAction,
default=True,
)
argparse erzeugt daraus automatisch:
--farbe
--no-farbe
Das ist praktischer, als zwei konkurrierende Flags von Hand zu verwalten.
Positionsargumente
Ein Argument ohne führenden Bindestrich ist positionsabhängig:
parser.add_argument(
"datei",
help="Pfad zum Spielstand",
)
Aufruf:
python dungeon_cli.py spielstand.json
Danach:
args.datei
mit:
"spielstand.json"
Fehlt ein benötigtes Positionsargument, meldet argparse den Fehler selbst.
Für ein CLI wirkt beispielsweise:
dungeon laden spielstand.json
meist natürlicher als:
dungeon laden --datei spielstand.json
weil die Datei der zentrale Gegenstand der Aktion laden ist.
Anders bei:
dungeon neu --name Karl --gold 50
name und gold konfigurieren hier den Start des Spiels.
Das ist keine harte Regel, aber eine gute Orientierung.
Pfade direkt als Path
Für Dateien können wir pathlib.Path verwenden:
from pathlib import Path
parser.add_argument(
"--laden",
type=Path,
help="Pfad zu einem Spielstand",
)
Danach enthält:
args.laden
direkt ein Path-Objekt.
Du kannst also schreiben:
if args.laden is not None:
print(args.laden.exists())
Wichtig ist der Unterschied zwischen Konvertierung und Validierung.
type=Path
macht aus dem String einen Path.
Es prüft nicht automatisch:
Existiert die Datei?
Ist sie lesbar?
Ist es wirklich eine Datei und kein Verzeichnis?
Das kannst Du nach dem Parsing kontrollieren.
Erlaubte Werte mit choices
Ein Schwierigkeitsgrad soll vielleicht nur drei Werte kennen:
parser.add_argument(
"--modus",
choices=[
"normal",
"schwer",
"alptraum",
],
default="normal",
)
Gültig:
python dungeon_cli.py --modus schwer
Ungültig:
python dungeon_cli.py --modus brutal
argparse meldet dann automatisch, welche Werte erlaubt sind.
Tippfehler vorschlagen: neu seit Python 3.14
Python 3.14 kann bei bestimmten falschen choices und Subcommand-Namen sogar
einen passenden Wert vorschlagen.
Dafür:
parser = argparse.ArgumentParser(
suggest_on_error=True,
)
Mit:
parser.add_argument(
"--modus",
choices=[
"normal",
"schwer",
"alptraum",
],
)
kann ein Tippfehler wie:
python dungeon_cli.py --modus normale
zusätzlich einen Hinweis auf einen ähnlichen erlaubten Wert bekommen.
suggest_on_error ist standardmäßig False und existiert seit Python 3.14.
Wenn Dein Projekt bewusst ältere Python-Versionen unterstützt, kannst Du die Option natürlich nicht einfach im Constructor voraussetzen.
Ein vollständiger Startparser
Trennen wir das Parsing von der eigentlichen Spiellogik:
import argparse
from pathlib import Path
def parse_args():
parser = argparse.ArgumentParser(
prog="dungeon",
description="Starte den Dungeon.",
suggest_on_error=True,
)
parser.add_argument(
"--name",
default="Abenteurer",
help="Name des Helden",
)
parser.add_argument(
"--gold",
type=int,
default=0,
help="Startgold des Helden",
)
parser.add_argument(
"--laden",
type=Path,
help="Pfad zu einem Spielstand",
)
parser.add_argument(
"--schwer",
action="store_true",
help="Startet im schweren Modus",
)
return parser.parse_args()
Die eigentliche Anwendung bekommt bereits fertige Python-Werte:
def starte_spiel(
name,
gold,
spielstand=None,
schwer=False,
):
print(f"Name: {name}")
print(f"Gold: {gold}")
if spielstand is not None:
print(f"Lade Spielstand aus {spielstand}")
if schwer:
print("Schwerer Modus aktiv.")
# Danach startet der eigentliche Game-Loop.
Und main() verbindet beide Seiten:
def main():
args = parse_args()
starte_spiel(
name=args.name,
gold=args.gold,
spielstand=args.laden,
schwer=args.schwer,
)
if __name__ == "__main__":
main()
Damit steckt die Kommandozeilenlogik nicht mitten im Spiel.
starte_spiel() lässt sich weiterhin direkt aus einem Test oder einem anderen
Python-Modul aufrufen.
Fachliche Fehler mit parser.error(...)
Ein Parser kann mehr prüfen als die reine Syntax der Kommandozeile.
Angenommen, negatives Startgold ist für unser CLI grundsätzlich ungültig:
args = parser.parse_args()
if args.gold < 0:
parser.error("--gold darf nicht negativ sein")
parser.error(...) erzeugt eine CLI-typische Fehlermeldung auf stderr und
beendet den Aufruf mit Exit-Code 2.
Das ist sinnvoll für Probleme mit dem Aufruf des CLI.
Für Fehler, die erst in der eigentlichen Anwendung auftreten, verwenden wir dagegen unsere normale Fehlerbehandlung.
Beispielsweise:
Spielstanddatei syntaktisch korrekt angegeben,
aber Datei enthält einen beschädigten Spielstand.
Das ist kein Parser-Fehler mehr.
Subcommands mit argparse
Unser Dungeon soll inzwischen mehrere Aktionen unterstützen:
dungeon neu --name Karl
dungeon laden spielstand.json
dungeon info spielstand.json
Dafür gibt es Subparser.
import argparse
from pathlib import Path
def befehl_neu(args):
print(f"Neues Spiel für {args.name}")
print(f"Startgold: {args.gold}")
if args.schwer:
print("Schwerer Modus aktiv.")
def befehl_laden(args):
print(f"Lade Spielstand aus {args.datei}")
def befehl_info(args):
print(f"Informationen zu {args.datei}")
def parse_args():
parser = argparse.ArgumentParser(
prog="dungeon",
description="Dungeon CLI",
suggest_on_error=True,
)
subparser = parser.add_subparsers(
required=True,
)
neu = subparser.add_parser(
"neu",
help="Startet ein neues Spiel",
)
neu.add_argument(
"--name",
default="Abenteurer",
help="Name des Helden",
)
neu.add_argument(
"--gold",
type=int,
default=0,
help="Startgold des Helden",
)
neu.add_argument(
"--schwer",
action="store_true",
help="Startet im schweren Modus",
)
neu.set_defaults(func=befehl_neu)
laden = subparser.add_parser(
"laden",
help="Lädt einen Spielstand",
)
laden.add_argument(
"datei",
type=Path,
help="Pfad zur Spielstanddatei",
)
laden.set_defaults(func=befehl_laden)
info = subparser.add_parser(
"info",
help="Zeigt Informationen zu einem Spielstand",
)
info.add_argument(
"datei",
type=Path,
help="Pfad zur Spielstanddatei",
)
info.set_defaults(func=befehl_info)
return parser.parse_args()
def main():
args = parse_args()
args.func(args)
if __name__ == "__main__":
main()
Jetzt funktionieren:
python spiel.py neu --name Karl --gold 50
python spiel.py laden spielstand.json
python spiel.py info spielstand.json
Das Muster:
neu.set_defaults(func=befehl_neu)
speichert die passende Funktion im Ergebnis des Parsers.
Dadurch braucht main() keine eigene Verzweigung:
if args.befehl == "neu":
...
elif args.befehl == "laden":
...
sondern nur:
args.func(args)
Warum required=True sinnvoll ist
Ohne:
parser.add_subparsers(
required=True,
)
ist ein Aufruf ohne Subcommand grundsätzlich möglich:
dungeon
Dann fehlt aber beispielsweise:
args.func
und wir müssten diesen Zustand separat behandeln.
Für unser CLI soll genau eine Aktion gewählt werden.
Deshalb passt:
required=True
hier gut.
Die Option gibt es seit Python 3.7.
Short Options
Neben langen Optionen:
--name
kannst Du eine Kurzform anbieten:
neu.add_argument(
"-n",
"--name",
default="Abenteurer",
)
Nun funktionieren beide:
dungeon neu --name Karl
dungeon neu -n Karl
Bei häufig verwendeten Flags ist das angenehm:
-v
--verbose
Bei einem seltenen Parameter wie:
--maximale-speicherdauer
musst Du dagegen nicht zwanghaft irgendeinen einzelnen Buchstaben reservieren.
Options-Abkürzungen von argparse
Standardmäßig erlaubt argparse eindeutige Abkürzungen langer Optionen.
Existiert nur:
--verbose
kann unter Umständen auch:
--verb
akzeptiert werden.
Bei einem kleinen privaten Skript kann das angenehm sein. Bei einem stabilen CLI kann es aber überraschend werden.
Fügst Du später eine zweite Option hinzu:
--verbosity
ist die bisher eindeutige Abkürzung plötzlich mehrdeutig.
Wenn Du nur exakt dokumentierte Optionsnamen akzeptieren möchtest:
parser = argparse.ArgumentParser(
allow_abbrev=False,
)
Dann gilt ausschließlich das tatsächlich definierte Interface.
Exit-Codes
Ein CLI kommuniziert nicht nur über Text.
Der aufrufende Prozess bekommt auch einen Exit-Code.
Konventionell bedeutet:
| Exit-Code | Bedeutung |
|---|---|
0 |
erfolgreich |
1 |
häufig allgemeiner Anwendungsfehler |
2 |
von argparse für CLI-Syntaxfehler |
Nur 0 ist die universell wichtige Aussage:
erfolgreich
Welche anderen Codes Dein eigenes Programm verwendet, kannst Du dokumentieren.
1 ist ein üblicher allgemeiner Fehlercode, aber kein Gesetz, das jede
Anwendung exakt so verwenden muss.
Eine main()-Funktion kann einen Code zurückgeben:
def main():
try:
starte_spiel()
except SpielstandError as fehler:
print(
f"Fehler: {fehler}",
file=sys.stderr,
)
return 1
return 0
Am Ende:
if __name__ == "__main__":
raise SystemExit(main())
Der Rückgabewert wird dadurch zum Exit-Code des Prozesses.
stdout und stderr
Ein ordentliches CLI hat zwei Ausgabekanäle.
Normale Ausgabe geht nach stdout:
print("Spielstand gespeichert.")
Fehlermeldungen nach stderr:
import sys
print(
"Fehler: Spielstand fehlt.",
file=sys.stderr,
)
Die Shell kann beide Streams getrennt behandeln:
dungeon info spielstand.json > ausgabe.txt 2> fehler.txt
ausgabe.txt bekommt stdout.
fehler.txt bekommt stderr.
Das wird besonders wichtig, wenn ein anderes Programm die normale Ausgabe weiterverarbeiten möchte:
dungeon info spielstand.json | grep "Gold"
Eine Fehlermeldung sollte dann nicht plötzlich zwischen den regulären Daten landen.
argparse schreibt seine eigenen Parsing-Fehler bereits nach stderr.
CLI-Ausgabe und Logging nicht verwechseln
Ein CLI kann zusätzlich Logging verwenden.
Diese Zeile:
print("Spielstand gespeichert.")
ist Teil der Benutzeroberfläche.
Diese:
logger.info(
"Spielstand gespeichert: datei=%s",
datei,
)
ist Diagnoseinformation.
Beides kann gleichzeitig sinnvoll sein.
Wie Logging aufgebaut wird, haben wir in Logging statt print behandelt.
Wann argparse völlig ausreicht
Für viele kleine Tools sieht ein CLI ungefähr so aus:
ein paar Optionen
ein oder zwei Subcommands
keine externe Dependency gewünscht
Dann gibt es wenig Grund, argparse sofort zu ersetzen.
Es ist Bestandteil von Python, stabil, gut dokumentiert und unterstützt wesentlich mehr, als wir hier benötigen.
Der Preis dafür ist hauptsächlich die Menge an Konfigurationscode:
parser.add_argument(...)
subparser.add_parser(...)
parser.set_defaults(...)
Bei einem größeren CLI wird dieser Teil schnell umfangreich.
Typer verfolgt deshalb einen anderen Ansatz.
Typer: CLI aus Funktionen und Type Hints
Typer ist eine externe Library und baut auf Click auf.
Installieren kannst Du sie mit uv:
uv add typer
Oder in einem klassischen Virtual Environment:
python -m pip install typer
Die normale Typer-Installation bringt heute auch die Dependencies für Rich-formatierte Ausgabe und Shell-Erkennung mit.
Das Grundprinzip ist einfach:
def neu(
name: str,
gold: int,
):
...
Die Python-Typen werden gleichzeitig zur Beschreibung des CLI.
Ein einzelner Typer-Befehl
Für ein CLI mit genau einer Aktion reicht sogar:
import typer
def main(
name: str = "Abenteurer",
gold: int = 0,
schwer: bool = False,
):
print(f"Name: {name}")
print(f"Gold: {gold}")
if schwer:
print("Schwerer Modus aktiv.")
if __name__ == "__main__":
typer.run(main)
Aufruf:
python dungeon_typer.py --name Karl --gold 50 --schwer
Typer erkennt:
name String
gold Integer
schwer Boolean
und erzeugt daraus die passenden CLI-Parameter.
Auch:
python dungeon_typer.py --help
funktioniert automatisch.
Für ein einziges Kommando ist typer.run(...) angenehm kompakt.
Mehrere Befehle mit Typer()
Unser Dungeon soll aber mehrere Aktionen bekommen:
dungeon neu
dungeon laden
dungeon info
Dann verwenden wir eine App:
import typer
app = typer.Typer(
help="Verwaltet den Dungeon.",
)
Commands registrieren wir mit Decorators:
@app.command()
def neu():
"""Startet ein neues Spiel."""
print("Neues Spiel")
Und am Ende:
if __name__ == "__main__":
app()
Eine wichtige Besonderheit bei genau einem Command
Angenommen, Du schreibst:
import typer
app = typer.Typer()
@app.command()
def neu():
print("Neues Spiel")
if __name__ == "__main__":
app()
Die App besitzt genau einen Command und keinen Callback.
Typer klappt diesen einzelnen Command dann zum Hauptkommando zusammen.
Der Aufruf lautet also:
python cli.py
und nicht:
python cli.py neu
Das ist praktisch für kleine CLIs.
Wenn neu ausdrücklich ein Subcommand bleiben soll, obwohl vorerst nur dieser
eine existiert, kannst Du einen Callback ergänzen:
import typer
app = typer.Typer()
@app.callback()
def haupt():
"""Verwaltet den Dungeon."""
@app.command()
def neu():
"""Startet ein neues Spiel."""
print("Neues Spiel")
if __name__ == "__main__":
app()
Dann ist:
python cli.py neu
korrekt.
Sobald mehrere Commands vorhanden sind, entsteht ohnehin eine Command Group.
Moderne Typer-Syntax mit Annotated
Typer unterstützt verschiedene Schreibweisen. In der aktuellen Dokumentation
wird für zusätzliche CLI-Metadaten bevorzugt Annotated verwendet.
from typing import Annotated
import typer
app = typer.Typer(
help="Dungeon CLI",
)
@app.command()
def neu(
name: Annotated[
str,
typer.Option(
"--name",
"-n",
help="Name des Helden",
),
] = "Abenteurer",
gold: Annotated[
int,
typer.Option(
help="Startgold des Helden",
),
] = 0,
):
print(f"Neues Spiel für {name}")
print(f"Startgold: {gold}")
if __name__ == "__main__":
app()
Der Python-Typ bleibt:
str
beziehungsweise:
int
Die Informationen für das CLI stecken zusätzlich in:
typer.Option(...)
Das ist angenehmer als ältere Schreibweisen, bei denen der Default-Wert selbst
ein typer.Option(...)-Objekt war.
Argumente in Typer
Ein Positionsargument deklarieren wir mit:
typer.Argument(...)
Für den Spielstand:
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def laden(
datei: Annotated[
Path,
typer.Argument(
help="Pfad zur Spielstanddatei",
),
],
):
print(f"Lade Spielstand aus {datei}")
if __name__ == "__main__":
app()
Aufruf:
python dungeon_typer.py laden spielstand.json
datei ist bereits ein Path.
Dateipfade direkt validieren
Typer kann bei Path-Parametern mehr prüfen als nur die Typkonvertierung.
Unser laden-Befehl verlangt eine existierende Datei:
@app.command()
def laden(
datei: Annotated[
Path,
typer.Argument(
help="Pfad zur Spielstanddatei",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
),
],
):
print(f"Lade Spielstand aus {datei}")
Damit kann Typer schon vor dem Funktionsaufruf ablehnen:
Pfad existiert nicht
Pfad zeigt auf ein Verzeichnis
Datei ist nicht lesbar
Weitere Optionen sind unter anderem:
writable
resolve_path
Ohne:
exists=True
bedeutet ein Path auch bei Typer nicht automatisch, dass der Pfad bereits
existieren muss.
Boolean-Flags in Typer
Typer erkennt Boolean-Optionen direkt am Typ.
@app.command()
def neu(
schwer: bool = False,
):
print(schwer)
Typer kann daraus automatisch positive und negative Varianten erzeugen:
--schwer
--no-schwer
Wenn Du nur ein einzelnes Flag möchtest, kannst Du den Optionsnamen ausdrücklich festlegen:
from typing import Annotated
import typer
@app.command()
def neu(
schwer: Annotated[
bool,
typer.Option(
"--schwer",
help="Startet im schweren Modus",
),
] = False,
):
if schwer:
print("Schwerer Modus aktiv.")
Nun gibt es bewusst nur:
--schwer
Eigene Namen für beide Boolean-Zustände
Für unseren Dungeon klingt:
--schwer / --normal
natürlicher als:
--schwer / --no-schwer
Das können wir ausdrücken:
@app.command()
def neu(
schwer: Annotated[
bool,
typer.Option(
"--schwer/--normal",
help="Wählt den Schwierigkeitsgrad",
),
] = False,
):
if schwer:
print("Schwerer Modus aktiv.")
else:
print("Normaler Modus aktiv.")
Aufrufe:
python dungeon_typer.py neu --schwer
python dungeon_typer.py neu --normal
Das liest sich wie ein echtes CLI und nicht wie ein Python-Bool auf der Kommandozeile.
Mehrere Commands mit Typer
Bauen wir die gleiche Struktur wie bei argparse:
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer(
help="Verwaltet den Dungeon.",
)
@app.command()
def neu(
name: Annotated[
str,
typer.Option(
"--name",
"-n",
help="Name des Helden",
),
] = "Abenteurer",
gold: Annotated[
int,
typer.Option(
help="Startgold des Helden",
),
] = 0,
schwer: Annotated[
bool,
typer.Option(
"--schwer",
help="Startet im schweren Modus",
),
] = False,
):
"""Startet ein neues Spiel."""
print(f"Neues Spiel für {name}")
print(f"Startgold: {gold}")
if schwer:
print("Schwerer Modus aktiv.")
@app.command()
def laden(
datei: Annotated[
Path,
typer.Argument(
help="Pfad zur Spielstanddatei",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
),
],
):
"""Lädt einen Spielstand."""
print(f"Lade Spielstand aus {datei}")
@app.command()
def info(
datei: Annotated[
Path,
typer.Argument(
help="Pfad zur Spielstanddatei",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
),
],
):
"""Zeigt Informationen zu einem Spielstand."""
print(f"Informationen zu {datei}")
if __name__ == "__main__":
app()
Aufrufe:
python dungeon_typer.py neu --name Karl --gold 50
python dungeon_typer.py laden spielstand.json
python dungeon_typer.py info spielstand.json
Die Docstrings werden gleichzeitig für die Command-Hilfe verwendet.
python dungeon_typer.py --help
python dungeon_typer.py laden --help
Typer validiert die CLI, nicht Deine gesamte Anwendung
Typer kann aus:
gold: int
einen Integer-Parameter machen.
Das bedeutet aber nicht, dass Deine komplette Anwendung plötzlich Runtime Type Checking besitzt.
Diese Funktion:
def starte_spiel(
name: str,
gold: int,
):
...
kann aus normalem Python-Code weiterhin direkt mit unsinnigen Werten aufgerufen werden:
starte_spiel(
name=123,
gold="viel",
)
Type Hints bleiben Type Hints.
Typer verwendet sie an der CLI-Grenze zum Parsen unterstützter Typen. Die fachlichen Regeln Deiner Anwendung musst Du weiterhin selbst definieren.
Wertebereiche in Typer
Einige typische Prüfungen lassen sich direkt an einer Option beschreiben.
Beispielsweise soll Startgold zwischen 0 und 1000 liegen:
@app.command()
def neu(
gold: Annotated[
int,
typer.Option(
min=0,
max=1000,
help="Startgold des Helden",
),
] = 0,
):
print(f"Startgold: {gold}")
Damit wird beispielsweise:
dungeon neu --gold -50
bereits von der CLI-Schicht abgelehnt.
Für komplizierte Game Rules würde ich trotzdem normale Python-Funktionen verwenden.
Eigene Parameterprüfung
Für spezielle CLI-Regeln kannst Du einen Callback verwenden.
Zum Beispiel:
def pruefe_name(name: str):
if not name.strip():
raise typer.BadParameter(
"Der Name darf nicht leer sein."
)
return name.strip()
Dann:
@app.command()
def neu(
name: Annotated[
str,
typer.Option(
callback=pruefe_name,
),
] = "Abenteurer",
):
print(f"Neues Spiel für {name}")
typer.BadParameter erzeugt eine passende CLI-Fehlermeldung.
Auch hier gilt: Ein Callback ist gut für eine Regel des Parameters.
Wenn die Validierung zehn andere Domain-Objekte benötigt, gehört sie wahrscheinlich tiefer in die Anwendung.
Fehler in Typer ausgeben
Normale Ausgabe kann weiterhin ganz gewöhnlich mit:
print(...)
erzeugt werden.
Typer stellt zusätzlich unter anderem:
typer.echo(...)
typer.secho(...)
bereit.
Für eine Fehlermeldung auf stderr:
typer.echo(
"Spielstand konnte nicht geladen werden.",
err=True,
)
Und ein eigener Exit-Code:
raise typer.Exit(code=1)
Zum Beispiel:
@app.command()
def laden(
datei: Path,
):
try:
lade_spielstand(datei)
except SpielstandError as fehler:
typer.echo(
f"Fehler: {fehler}",
err=True,
)
raise typer.Exit(code=1) from None
Damit bleibt auch ein Typer-CLI für Shell-Skripte sauber auswertbar.
CLI-Schicht und Spiellogik trennen
Ob argparse oder Typer: Die wichtigste Architekturentscheidung hat mit dem
Parser selbst wenig zu tun.
Ungünstig:
@app.command()
def neu(name: str = "Abenteurer"):
# 300 Zeilen Game-Loop
...
Besser:
@app.command()
def neu(
name: str = "Abenteurer",
):
starte_neues_spiel(
name=name,
)
Die Funktion:
starte_neues_spiel(...)
sollte nichts über Typer wissen müssen.
Sie bekommt normale Python-Werte.
Das hat mehrere Vorteile.
Ein Test kann schreiben:
starte_neues_spiel(
name="Karl",
)
Eine grafische Oberfläche könnte später dieselbe Funktion verwenden.
Und wenn Du irgendwann Typer gegen ein anderes CLI-Framework austauschst, bleibt die eigentliche Anwendung unangetastet.
Unser Dungeon als Anwendung
Eine mögliche Aufteilung:
dungeon/
├── cli.py
├── spiel.py
├── modelle.py
└── speichern.py
spiel.py:
def starte_neues_spiel(
name,
gold,
schwer=False,
):
...
speichern.py:
def lade_spielstand(datei):
...
cli.py enthält nur die Verbindung zur Kommandozeile:
from pathlib import Path
from typing import Annotated
import typer
from dungeon.spiel import starte_neues_spiel
from dungeon.speichern import lade_spielstand
app = typer.Typer(
help="Verwaltet den Dungeon.",
)
@app.command()
def neu(
name: Annotated[
str,
typer.Option(
"--name",
"-n",
help="Name des Helden",
),
] = "Abenteurer",
gold: Annotated[
int,
typer.Option(
min=0,
help="Startgold des Helden",
),
] = 0,
schwer: Annotated[
bool,
typer.Option(
"--schwer",
help="Startet im schweren Modus",
),
] = False,
):
"""Startet ein neues Spiel."""
starte_neues_spiel(
name=name,
gold=gold,
schwer=schwer,
)
@app.command()
def laden(
datei: Annotated[
Path,
typer.Argument(
help="Pfad zum Spielstand",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
),
],
):
"""Lädt einen Spielstand."""
lade_spielstand(datei)
def main():
app()
if __name__ == "__main__":
main()
Die CLI-Funktion ist jetzt dünn.
Das ist ein gutes Zeichen.
Vom Python-Modul zum echten dungeon-Kommando
Bisher starten wir noch:
python dungeon_typer.py neu
oder:
python -m dungeon.cli neu
Ein richtig installiertes CLI möchten wir aber so benutzen:
dungeon neu --name Karl
Dafür braucht das installierbare Python-Projekt einen Entry Point.
In pyproject.toml:
[project]
name = "dungeon"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"typer",
]
[project.scripts]
dungeon = "dungeon.cli:main"
Die rechte Seite bedeutet:
Python-Modul:Callable
Also:
dungeon.cli
│
└── main()
Unsere Funktion:
def main():
app()
ist damit der Einstiegspunkt.
Wichtig: Damit ein [project.scripts]-Entry-Point tatsächlich installiert
werden kann, braucht das Projekt ein Build-System und muss als Package
installierbar sein.
Ein aktuelles:
uv init dungeon
erzeugt inzwischen standardmäßig genau so ein paketiertes Application-Projekt
mit Build-System und src/-Layout.
Mehr dazu findest Du in uv: pip und venv in schnell.
Ein mögliches Projektlayout
Mit einem src-Layout könnte unser Projekt so aussehen:
dungeon/
├── pyproject.toml
├── README.md
└── src/
└── dungeon/
├── __init__.py
├── cli.py
├── spiel.py
├── modelle.py
└── speichern.py
Danach kann uv das Projekt in sein Environment installieren:
uv sync
Und das Entry-Point-Kommando lässt sich direkt starten:
uv run dungeon neu --name Karl
Ist das Environment aktiviert beziehungsweise das Package anderweitig installiert, steht entsprechend auch:
dungeon neu --name Karl
zur Verfügung.
Damit sind wir wirklich vom:
python spiel.py
zum installierbaren CLI gekommen.
Shell Completion mit Typer
Typer bringt Unterstützung für Shell Completion mit.
Bei einer normalen Typer-App erscheinen in der Hilfe typischerweise Optionen wie:
--install-completion
--show-completion
Dadurch kann die Shell beispielsweise Command- und Optionsnamen vervollständigen.
Das ist bei einem kleinen Dungeon Spielerei.
Bei einem CLI mit vielen Subcommands und Optionen wird:
dungeon lo<TAB>
aber schnell angenehmer als jedes Kommando vollständig einzutippen.
Wenn Du diese Typer-spezifischen Completion-Optionen nicht möchtest:
app = typer.Typer(
add_completion=False,
)
argparse oder Typer?
Die Entscheidung ist weniger dramatisch, als sie manchmal dargestellt wird.
argparse ist kein veralteter Vorgänger, den moderne Projekte grundsätzlich
ersetzen müssten.
Typer ist auch nicht automatisch die richtige Lösung für jedes Zwanzig-Zeilen-Skript.
argparse passt gut, wenn …
Du keine externe Dependency möchtest und das CLI überschaubar bleibt.
Ein kleines Admin-Skript mit:
--config
--verbose
--dry-run
braucht nicht zwingend ein Framework.
argparse ist außerdem überall verfügbar, wo Python selbst vorhanden ist.
Typer passt gut, wenn …
das CLI mehrere Commands bekommt, Du ohnehin Type Hints verwendest und möglichst wenig Parser-Boilerplate schreiben möchtest.
Aus:
def laden(
datei: Path,
verbose: bool = False,
):
...
direkt ein CLI ableiten zu können, ist angenehm.
Hinzu kommen Rich-formatierte Hilfe, Completion und die Möglichkeiten des darunterliegenden Click-Ökosystems.
Typer baut auf Click auf
Typer ersetzt Click nicht vollständig durch eine eigene CLI-Engine.
Es erzeugt auf Grundlage Deiner Funktionen und Type Hints passende Click-Strukturen.
Deshalb tauchen bei fortgeschrittener Nutzung Konzepte wie:
Context
Callbacks
Parameter
Command Groups
Exit
Abort
aus Click wieder auf.
Für viele CLIs musst Du davon kaum etwas wissen.
Wenn Du aber an Grenzen von Typer stößt oder sehr genaue Kontrolle über das CLI-Verhalten brauchst, lohnt sich der Blick in Click: Kommandozeilen-Tools mit Decorators bauen.
Nicht gleichzeitig zwei Parser pflegen
Ein Projekt sollte normalerweise nicht dieselben Commands parallel in:
argparse
und:
Typer
definieren.
Dann hättest Du zwei Stellen für:
Optionsnamen
Defaults
Validierung
Hilfetexte
Subcommands
Das wird schnell inkonsistent.
Der Vergleich in diesem Artikel zeigt zwei mögliche Wege.
Für das tatsächliche Projekt entscheidest Du Dich anschließend für einen.
Ein vorhandenes funktionierendes argparse-CLI muss auch nicht nur deshalb
migriert werden, weil Typer weniger Zeilen benötigt.
Interaktive und nicht-interaktive Nutzung kombinieren
Unser Dungeon ist ein Sonderfall: Nach dem CLI-Start soll das Spiel selbst weiter interaktiv sein.
Das ist kein Problem.
Beispielsweise:
dungeon neu --name Karl --gold 50
liefert die Startkonfiguration.
Danach beginnt der bekannte Game-Loop:
Du stehst in einer dunklen Halle.
> norden
> umsehen
> nimm fackel
Was Du vermeiden möchtest, ist eine Automatisierung, bei der nicht erkennbar ist, dass noch weitere Eingaben verlangt werden.
Für einen vollständig nicht-interaktiven Modus könnte man später etwa anbieten:
dungeon info spielstand.json
oder:
dungeon validiere spielstand.json
Diese Commands geben ihr Ergebnis aus und beenden sich wieder.
Fehlende Werte optional abfragen
Manchmal soll ein Wert entweder als Option kommen oder interaktiv erfragt werden.
Zum Beispiel:
def frage_falls_fehlt(
wert,
text,
):
if wert is not None:
return wert
return input(text)
Dann könnte:
dungeon neu --name Karl
ohne weitere Namensfrage starten.
Bei:
dungeon neu
fragt das Programm dagegen nach.
Das kann für ein Spiel angenehm sein.
Bei einem Admin-Tool wäre ich damit vorsichtiger. Automatisierbare Commands sollten idealerweise einen vollständig nicht-interaktiven Aufruf ermöglichen.
Gute CLI-Funktionen geben Ergebnisse nicht nur aus
Noch besser testbar wird Code, wenn die Anwendungslogik nicht selbst überall
print() verwendet.
Statt:
def zeige_info(datei):
print("Gold: 50")
könnte die fachliche Funktion Daten liefern:
def lese_info(datei):
return {
"spieler": "Karl",
"gold": 50,
}
Die CLI entscheidet über die Darstellung:
daten = lese_info(datei)
print(f"Spieler: {daten['spieler']}")
print(f"Gold: {daten['gold']}")
Später könntest Du problemlos zusätzlich anbieten:
dungeon info spielstand.json --json
ohne die Ladefunktion neu schreiben zu müssen.
Die CLI ist die äußere Schicht. Sie übersetzt zwischen Kommandozeile und Anwendung.
Maschinell lesbare Ausgabe mitdenken
Sobald ein CLI automatisiert benutzt wird, zählt nicht nur, ob die Ausgabe für Menschen hübsch aussieht.
Angenommen:
dungeon info spielstand.json
liefert:
Spieler: Karl
Gold: 50
Ort: Halle
Das ist angenehm zu lesen.
Für andere Programme wäre vielleicht besser:
dungeon info spielstand.json --json
mit:
{
"spieler": "Karl",
"gold": 50,
"ort": "Halle"
}
Der wichtige Designgedanke ist:
stdout ist Teil der öffentlichen Schnittstelle eines CLI.
Wenn andere Skripte diese Ausgabe parsen, solltest Du sie nicht bei jeder Version willkürlich verändern.
Fehlertexte sind für Menschen, Exit-Codes für Programme
Ein Shell-Skript sollte nicht so prüfen:
if dungeon laden spielstand.json | grep -q "Fehler"; then
...
fi
Dafür gibt es Exit-Codes:
if dungeon laden spielstand.json; then
echo "Erfolgreich"
else
echo "Fehlgeschlagen"
fi
Der Mensch bekommt zusätzlich:
Fehler: Spielstanddatei ist beschädigt.
auf stderr.
Beide Schnittstellen ergänzen sich.
Häufige Stolperfallen
-
CLI und Spiellogik vermischen: Der Parser sollte Parameter entgegennehmen und normale Anwendungsfunktionen aufrufen. Der komplette Game-Loop gehört nicht in eine Typer-Command-Funktion.
-
sys.argvvon Hand zerlegen: Für mehr als triviale Spezialfälle ist ein Parser einfacher und robuster. -
type=boolmitargparseverwenden:"False"ist ein nicht leerer String und damit fürbool()wahr. Verwende Flags oderBooleanOptionalAction. -
type=Pathmit Existenzprüfung verwechseln: EinPath-Objekt kann auf einen nicht vorhandenen Pfad zeigen. -
Komplexe Validierung in
argparse type=quetschen: Einfache Konvertierungen gehören dort hin, umfangreiche Fachlogik nicht. -
Zu viele Positionsargumente verwenden: Ein Aufruf wie
tool Karl 50 true config.jsonist ohne Dokumentation kaum verständlich. -
Jeden Pflichtwert als
--optiondefinieren: Ein zentraler Dateiname oder eine ID kann als Positionsargument natürlicher sein. -
argparse-Subcommands ohnerequired=Trueverwenden und anschließendargs.funcvoraussetzen: Der Aufruf ohne Subcommand muss dann separat behandelt werden. -
Unbeabsichtigte Options-Abkürzungen übersehen: Wenn ein stabiles CLI nur exakte Namen akzeptieren soll, setze
allow_abbrev=False. -
Python-3.14-Features voraussetzen, obwohl das Projekt noch 3.12 unterstützt:
suggest_on_errorund die neuecolor-Option gehören erst zu Python 3.14. -
stdout und stderr vermischen: Normale Daten nach stdout, Fehler und Diagnose nach stderr.
-
Nur Text ausgeben und Exit-Codes ignorieren: Andere Programme brauchen eine zuverlässige Erfolgsmeldung, die nicht aus Text erraten werden muss.
-
1für den einzig erlaubten Fehlercode halten: Es ist ein üblicher allgemeiner Wert. Ein CLI darf weitere sinnvoll dokumentierte Codes verwenden. -
Bei Typer einen einzelnen
@app.command()automatisch als Subcommand erwarten: Bei genau einem Command ohne Callback klappt Typer ihn zum Hauptkommando zusammen. -
Boolean-Optionen in Typer falsch einschätzen: Ein einfacher
boolkann positive und negative Flags erzeugen. Wenn Du nur--schwermöchtest, definiere diesen Namen ausdrücklich. -
Type Hints mit allgemeiner Runtime-Validierung verwechseln: Typer nutzt sie für die CLI-Grenze. Normale Python-Aufrufe werden dadurch nicht automatisch validiert.
-
Typer-spezifische Regeln in die Spiellogik ziehen: Die Domain sollte möglichst nicht von
typer.Option,typer.BadParameteroder Click-Kontexten abhängen. -
Typer und
argparseparallel für dieselben Commands pflegen: Entscheide Dich für eine öffentliche CLI-Definition. -
[project.scripts]ohne installierbares Package erwarten: Entry Points müssen von einem Build-System installiert werden. -
Logging als normale CLI-Ausgabe verwenden: Logs und stdout/stderr der Benutzeroberfläche erfüllen unterschiedliche Aufgaben.
-
Interaktive Eingaben in einen vermeintlich automatisierbaren Command einbauen: Dokumentiere Interaktivität oder biete einen vollständig nicht-interaktiven Weg an.
Kompakte Gegenüberstellung
| Aufgabe | argparse |
Typer |
|---|---|---|
| Installation | Standardbibliothek | externe Dependency |
| Parameter definieren | add_argument(...) |
Funktionsparameter und Type Hints |
| String → Integer | type=int |
gold: int |
| Positionsargument | add_argument("datei") |
typer.Argument(...) |
| Option | add_argument("--name") |
typer.Option(...) |
| Boolean-Flag | action="store_true" |
bool beziehungsweise typer.Option(...) |
--foo/--no-foo |
BooleanOptionalAction |
bei Bool-Optionen direkt unterstützt |
| erlaubte Werte | choices=... |
passende Typen/Option-Konfiguration |
| Dateipfad | type=Path |
Path |
| Path validieren | eigene Prüfung | exists=, file_okay=, readable= usw. |
| Subcommands | add_subparsers() |
@app.command() |
| Help | automatisch | automatisch |
| Shell Completion | nicht als vergleichbarer Komplettworkflow | eingebaut |
| Parsing-Fehler | standardmäßig Exit-Code 2 |
Click/Typer übernimmt CLI-Fehler |
| eigene Fehler | stderr + SystemExit |
z. B. typer.echo(..., err=True) + Exit |
| kein zusätzliches Package | großer Vorteil | nein |
| Boilerplate bei großem CLI | vergleichsweise viel | meist deutlich weniger |
Übungen
1. Ein kleines argparse-CLI
Schreibe hallo_argparse.py, das eine Option --name akzeptiert und:
Hallo, <Name>!
ausgibt.
Lösung
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--name",
default="Welt",
help="Name für die Begrüßung",
)
args = parser.parse_args()
print(f"Hallo, {args.name}!")
if __name__ == "__main__":
main()
python hallo_argparse.py --name Karl
2. Einen Boolean-Schalter ergänzen
Ergänze --laut, sodass die Begrüßung in Großbuchstaben erscheint.
Lösung
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--name",
default="Welt",
)
parser.add_argument(
"--laut",
action="store_true",
help="Gibt die Begrüßung in Großbuchstaben aus",
)
args = parser.parse_args()
text = f"Hallo, {args.name}!"
if args.laut:
text = text.upper()
print(text)
if __name__ == "__main__":
main()
python hallo_argparse.py --name Karl --laut
3. Zwei argparse-Subcommands
Baue die Befehle:
neu
laden
laden soll einen Dateipfad als Positionsargument bekommen.
Lösung
import argparse
from pathlib import Path
def befehl_neu(args):
print(f"Neues Spiel für {args.name}")
def befehl_laden(args):
print(f"Lade {args.datei}")
def main():
parser = argparse.ArgumentParser(
prog="dungeon",
)
subparser = parser.add_subparsers(
required=True,
)
neu = subparser.add_parser("neu")
neu.add_argument(
"--name",
default="Abenteurer",
)
neu.set_defaults(
func=befehl_neu,
)
laden = subparser.add_parser("laden")
laden.add_argument(
"datei",
type=Path,
)
laden.set_defaults(
func=befehl_laden,
)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
python cli.py neu --name Karl
python cli.py laden spielstand.json
4. Python-3.14-Vorschläge aktivieren
Aktiviere bei einem argparse-CLI Vorschläge für vertippte choices.
Lösung
import argparse
parser = argparse.ArgumentParser(
suggest_on_error=True,
)
parser.add_argument(
"--modus",
choices=[
"normal",
"schwer",
"alptraum",
],
)
args = parser.parse_args()
5. Einen Typer-Command schreiben
Erstelle ein Typer-CLI mit dem Subcommand:
neu
und der Option:
--name
Lösung
from typing import Annotated
import typer
app = typer.Typer()
@app.callback()
def haupt():
"""Verwaltet den Dungeon."""
@app.command()
def neu(
name: Annotated[
str,
typer.Option(
"--name",
help="Name des Helden",
),
] = "Abenteurer",
):
print(f"Neues Spiel für {name}")
if __name__ == "__main__":
app()
uv add typer
uv run python cli.py neu --name Karl
6. Einen Spielstand mit Typer validieren
Schreibe einen Command:
dungeon laden spielstand.json
Der Pfad muss existieren und eine Datei sein.
Lösung
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def laden(
datei: Annotated[
Path,
typer.Argument(
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
),
],
):
print(f"Lade {datei}")
if __name__ == "__main__":
app()
7. Einen Fehler korrekt melden
Schreibe mit Typer eine Fehlermeldung nach stderr und beende das Programm mit
Exit-Code 1.
Lösung
import typer
def fehler():
typer.echo(
"Spielstand konnte nicht geladen werden.",
err=True,
)
raise typer.Exit(code=1)
Weiterlesen
- Python lernen, Teil 10: Module und Projektstruktur
- uv: pip und venv in schnell
- Ruff: Linter und Formatter in einem
- Logging statt print
- Type Hints mit mypy und pyright
- Click: Kommandozeilen-Tools mit Decorators bauen
- Python-Dokumentation:
argparse - Python-Dokumentation: argparse Tutorial
- Python-Dokumentation:
__main__ - Python-Dokumentation:
sys.argv - Typer-Dokumentation
- Typer: First Steps
- Typer: Arguments
- Typer: Options
- Typer: Commands
- Typer: Path
- uv: Entry Points und Build-Systeme
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.