CLI nástroj v Pythonu: argparse vs. Typer na jednom příkladu
Jak napsat příkazovou řádku v Pythonu – argumenty, volby, podpříkazy a návratové kódy. Stejný nástroj v argparse ze standardní knihovny i v moderním Typeru.
Skript s natvrdo zapsanými cestami je fajn pro jedno použití. Jakmile ho chcete spouštět s různými parametry nebo ho dát kolegům, potřebuje pořádné rozhraní příkazové řádky: nápovědu, validaci vstupů a srozumitelné chyby. Postavíme stejný nástroj dvakrát – v argparse a v Typeru – a porovnáme je.
Zadání
Nástroj velikost vypíše největší soubory ve složce:
velikost scan ./projekt --top 10 --pripona .log
velikost smaz ./projekt --starsi-nez 30 --nanecisto
Varianta 1: argparse
Součást standardní knihovny, žádné závislosti.
#!/usr/bin/env python3
import argparse
import sys
import time
from pathlib import Path
def cmd_scan(args: argparse.Namespace) -> int:
vzor = f"*{args.pripona}" if args.pripona else "*"
soubory = [p for p in args.slozka.rglob(vzor) if p.is_file()]
soubory.sort(key=lambda p: p.stat().st_size, reverse=True)
for p in soubory[: args.top]:
print(f"{p.stat().st_size / 1_048_576:8.1f} MB {p}")
return 0
def cmd_smaz(args: argparse.Namespace) -> int:
hranice = time.time() - args.starsi_nez * 86400
for p in args.slozka.rglob("*"):
if p.is_file() and p.stat().st_mtime < hranice:
print(("[nanečisto] " if args.nanecisto else "") + f"mažu {p}")
if not args.nanecisto:
p.unlink()
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="velikost", description="Práce s velkými soubory.")
parser.add_argument("-v", "--verbose", action="store_true", help="podrobný výstup")
sub = parser.add_subparsers(dest="prikaz", required=True)
p_scan = sub.add_parser("scan", help="vypíše největší soubory")
p_scan.add_argument("slozka", type=Path)
p_scan.add_argument("--top", type=int, default=20, help="počet souborů (výchozí 20)")
p_scan.add_argument("--pripona", help="např. .log")
p_scan.set_defaults(func=cmd_scan)
p_smaz = sub.add_parser("smaz", help="smaže staré soubory")
p_smaz.add_argument("slozka", type=Path)
p_smaz.add_argument("--starsi-nez", type=int, required=True, metavar="DNI")
p_smaz.add_argument("--nanecisto", action="store_true")
p_smaz.set_defaults(func=cmd_smaz)
args = parser.parse_args(argv)
if not args.slozka.is_dir():
parser.error(f"{args.slozka} není složka")
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
Poznámky:
--starsi-nezse vargsobjeví jakostarsi_nez(pomlčky → podtržítka).parser.error()vypíše usage a skončí s kódem 2, což je konvence pro chybné použití.set_defaults(func=...)je idiomatický způsob, jak přiřadit podpříkazu funkci.main(argv)s parametrem jde snadno testovat:main(["scan", "/tmp", "--top", "3"]).
Varianta 2: Typer
Typer (od autora FastAPI) odvozuje rozhraní z typových anotací. Postaven je na knihovně Click.
pip install typer
#!/usr/bin/env python3
import time
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer(help="Práce s velkými soubory.")
Slozka = Annotated[Path, typer.Argument(exists=True, file_okay=False, dir_okay=True)]
@app.command()
def scan(
slozka: Slozka,
top: Annotated[int, typer.Option(min=1, help="Počet souborů")] = 20,
pripona: Annotated[str | None, typer.Option(help="Např. .log")] = None,
):
"""Vypíše největší soubory."""
vzor = f"*{pripona}" if pripona else "*"
soubory = sorted(
(p for p in slozka.rglob(vzor) if p.is_file()),
key=lambda p: p.stat().st_size, reverse=True,
)
for p in soubory[:top]:
typer.echo(f"{p.stat().st_size / 1_048_576:8.1f} MB {p}")
@app.command()
def smaz(
slozka: Slozka,
starsi_nez: Annotated[int, typer.Option(help="Stáří ve dnech")],
nanecisto: bool = False,
):
"""Smaže staré soubory."""
hranice = time.time() - starsi_nez * 86400
for p in slozka.rglob("*"):
if p.is_file() and p.stat().st_mtime < hranice:
typer.echo(("[nanečisto] " if nanecisto else "") + f"mažu {p}")
if not nanecisto:
p.unlink()
if __name__ == "__main__":
app()
Co dostanete zadarmo:
starsi_nezbez výchozí hodnoty je povinná volba--starsi-nez.nanecisto: bool = Falsevytvoří dvojici--nanecisto / --no-nanecisto.exists=Trueověří existenci složky a vypíše hezkou chybu.- Docstring funkce se stane nápovědou příkazu.
- Doplňování v shellu:
velikost --install-completion(po instalaci jako balíček).
Pro ukončení s chybou použijte raise typer.Exit(code=1).
Srovnání
| argparse | Typer | |
|---|---|---|
| Závislosti | žádné | typer (+ click, volitelně rich) |
| Množství kódu | víc | méně |
| Validace | ruční / type= |
z anotací, min, exists |
| Barevná nápověda | ne (od Pythonu 3.14 volitelně) | ano, s nainstalovaným rich |
| Vhodné pro | skripty na servery, kde nechcete závislosti | větší nástroje s mnoha příkazy |
Instalace jako příkaz
V pyproject.toml definujte vstupní bod:
[project.scripts]
velikost = "velikost.cli:app" # Typer
# velikost = "velikost.cli:main" # argparse
Po pip install -e . nebo uv tool install . je příkaz velikost dostupný v PATH.
Konvence, které uživatelé ocení
- Návratový kód: 0 = úspěch, nenulový = chyba. Skripty a CI se podle toho řídí.
- Chyby na stderr, data na stdout:
print("chyba", file=sys.stderr)nebotyper.echo(..., err=True). Výstup pak jde bezpečně přesměrovat do souboru nebogrep. - Destruktivní akce s potvrzením nebo
--nanecisto. V Typeru stačítyper.confirm("Opravdu smazat?", abort=True). --versionse hodí u nástrojů, které distribuujete.
Pro jednorázový skript zvolte argparse, pro nástroj, který bude růst, Typer.