Programování

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.

12. 10. 2026 · 4 min čtení

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-nez se v args objeví jako starsi_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_nez bez výchozí hodnoty je povinná volba --starsi-nez.
  • nanecisto: bool = False vytvoří dvojici --nanecisto / --no-nanecisto.
  • exists=True ověří 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) nebo typer.echo(..., err=True). Výstup pak jde bezpečně přesměrovat do souboru nebo grep.
  • Destruktivní akce s potvrzením nebo --nanecisto. V Typeru stačí typer.confirm("Opravdu smazat?", abort=True).
  • --version se hodí u nástrojů, které distribuujete.

Pro jednorázový skript zvolte argparse, pro nástroj, který bude růst, Typer.