Mai devreme sau mai târziu, fiecare proiect Python își crește un punct de intrare în linia de comandă. Un data engineer are nevoie de un mod să declanșeze un backfill din terminal. Un autor de bibliotecă vrea mytool --version. O echipă are trei scripturi care toate vor flag-uri comune. Pattern-ul e atât de consistent încât există trei biblioteci bine stabilite pentru el, iar cea pe care o alegi depinde mai ales de cât de mult ții la ergonomie și cât de mare va deveni suprafața.
Lecția asta e cele trei opțiuni, argparse, click, typer, scrise una lângă alta ca să fie vizibile compromisurile. Apoi un exemplu puțin mai mare, apoi cum să livrezi rezultatul ca o comandă reală instalabilă.
argparse: stdlib, fără dependențe
argparse e în biblioteca standard. Ăsta e principalul lui argument de vânzare: zero instalare, disponibil peste tot. Costul e un API verbos, declarativ, care se simte mai greu decât ar trebui pentru ce faci.
# greet_argparse.py
import argparse
def main() -> None:
parser = argparse.ArgumentParser(
prog="greet",
description="Greet a person by name.",
)
parser.add_argument("name", help="who to greet")
parser.add_argument("--greeting", default="Hello", help="the greeting word")
parser.add_argument("--count", type=int, default=1, help="how many times")
parser.add_argument("--shout", action="store_true", help="uppercase the output")
args: argparse.Namespace = parser.parse_args()
line: str = f"{args.greeting}, {args.name}!"
if args.shout:
line = line.upper()
for _ in range(args.count):
print(line)
if __name__ == "__main__":
main()
$ python greet_argparse.py Marco --greeting Ciao --count 2 --shout
CIAO, MARCO!
CIAO, MARCO!
Lucruri de observat:
- Fiecare
add_argumente un apel de metodă cu argumente poziționale + cuvânt cheie. Verbos, dar explicit. argse un obiectNamespace,args.name,args.greetingetc., neverificat la tip. Hint-urile de tip din funcția ta nu te ajută aici.--helpe generat automat. Asta e singura zonă unde argparse e cu adevărat bun.
Când e argparse în regulă: un singur script cu cinci flag-uri sau mai puține și nu vrei o dependență. Pentru orice mai mult de atât, scăderea de productivitate față de click/typer e reală.
click: bazat pe decoratori, calul de povară
click e un pachet terț (pip install click) care e biblioteca CLI de facto pentru Python de peste un deceniu. Pattern-ul e bazat pe decoratori, abstracțiile sunt mature și o porțiune uriașă din ecosistemul Python e construită pe el. pip în sine e o aplicație click.
# greet_click.py
import click
@click.command()
@click.argument("name")
@click.option("--greeting", default="Hello", help="the greeting word")
@click.option("--count", default=1, type=int, help="how many times")
@click.option("--shout", is_flag=True, help="uppercase the output")
def main(name: str, greeting: str, count: int, shout: bool) -> None:
"""Greet a person by name."""
line: str = f"{greeting}, {name}!"
if shout:
line = line.upper()
for _ in range(count):
click.echo(line)
if __name__ == "__main__":
main()
Aceeași unealtă, formă diferită:
- Semnătura funcției este definiția CLI-ului. Fiecare
@click.optionși@click.argumentadaugă un parametru. - Funcția primește argumente Python normale, nu un Namespace. Curg în codul tău cu numele și tipurile pe care le-ai declarat.
click.echoîn loc deprint, singura diferență practică e că tratează cazurile limită de encoding și respectă test runner-ele care capturează output-ul.
Marele câștig al lui click sunt subcomenzile. CLI-urile reale sunt rareori comenzi singulare; sunt git commit, git push, git status. @click.group din click face asta curat:
import click
@click.group()
def cli() -> None:
"""Mytool: do things."""
@cli.command()
@click.option("--port", default=8000)
def serve(port: int) -> None:
"""Run the server."""
click.echo(f"serving on port {port}")
@cli.command()
@click.argument("target")
def migrate(target: str) -> None:
"""Apply migrations up to TARGET."""
click.echo(f"migrating to {target}")
@cli.command()
def status() -> None:
"""Show current status."""
click.echo("everything is fine")
if __name__ == "__main__":
cli()
$ python mytool.py --help
$ python mytool.py serve --port 9000
$ python mytool.py migrate v3
$ python mytool.py status
Fiecare subcomandă are propriul --help, propriile argumente, propria logică. Grupurile pot fi imbricate: un grup cli care conține un grup db care conține subcomenzile migrate, dump, restore.
Pentru CLI-uri mijlocii spre mari, click e alegerea sigură. E stabil, bine documentat, bine susținut, iar pattern-urile sunt previzibile.
typer: bazat pe hint-uri de tip, alegerea modernă
typer (pip install typer) e construit deasupra lui click. Teza e simplă: dacă scrii deja hint-uri de tip, biblioteca CLI ar trebui să le citească și să sară peste boilerplate-ul de decoratori.
# greet_typer.py
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: Annotated[str, typer.Argument(help="who to greet")],
greeting: Annotated[str, typer.Option(help="the greeting word")] = "Hello",
count: Annotated[int, typer.Option(help="how many times")] = 1,
shout: Annotated[bool, typer.Option(help="uppercase the output")] = False,
) -> None:
"""Greet a person by name."""
line: str = f"{greeting}, {name}!"
if shout:
line = line.upper()
for _ in range(count):
typer.echo(line)
if __name__ == "__main__":
app()
Câteva lucruri de observat:
- Hint-urile de tip (
int,bool,str) conduc parsarea. Nu scriitype=int; typer citește adnotarea. Annotated[T, typer.Option(...)]atașează metadate de CLI fără să polueze tipul de runtime. Ăsta e stilul modern; codul typer mai vechi foloseștename: str = typer.Argument(...)ca valoare implicită, ceea ce funcționează, dar e mai puțin curat.- Valorile implicite din semnătură devin valorile implicite în CLI.
- Docstring-ul devine automat descrierea
--help.
Subcomenzile funcționează la fel ca grupurile click, doar cu @app.command():
import typer
app = typer.Typer()
@app.command()
def serve(port: int = 8000) -> None:
"""Run the server."""
typer.echo(f"serving on port {port}")
@app.command()
def migrate(target: str) -> None:
"""Apply migrations up to TARGET."""
typer.echo(f"migrating to {target}")
@app.command()
def status() -> None:
"""Show current status."""
typer.echo("everything is fine")
if __name__ == "__main__":
app()
Argumentul de vânzare al typer e că aproape nu există cod specific CLI-ului: funcția e funcția, tipurile sunt parsarea, docstring-ul e ajutorul. Într-un codebase în care orice altceva e cu hint-uri de tip, typer e opțiunea care se potrivește stilului. În 2026, majoritatea CLI-urilor Python noi folosesc typer implicit.
Una lângă alta: aceeași unealtă, trei biblioteci
Unealta de salut de mai sus apare în toate cele trei variante. Numărând linii de schelet CLI care nu sunt importuri și nu sunt comentarii:
- argparse: ~7 linii de
add_argumentplus parsarea + tratarea namespace-ului. - click: 3 linii de decoratori plus o semnătură de funcție.
- typer: o semnătură de funcție, punct.
Pentru o unealtă cu cinci flag-uri, diferența e mică. Pentru o unealtă cu cincizeci de flag-uri și cinci subcomenzi, abordările cu decoratori/hint-uri de tip scalează dramatic mai bine.
Când să alegi ce:
- argparse — cu adevărat nu poți lua o dependență (tooling pentru distribuția Python, un script unic într-un mediu constrâns) sau CLI-ul e atât de banal încât costul importului bate câștigul de productivitate.
- click — vrei o bibliotecă matură, stabilă, testată în luptă; lucrezi pe un codebase care folosește deja click; ai nevoie de o funcționalitate pe care typer încă n-a expus-o.
- typer — orice e nou, mai ales cod cu multe hint-uri de tip, mai ales proiecte de data engineering și ML.
Help auto-generat
Toate trei îți dau --help gratis. Calitatea diferă:
- argparse: funcțional, formatare ocazional stângace.
- click: șlefuit, suportă panouri rich de help, se integrează cu
click-help-colorspentru culoare. - typer: construit pe click, plus extrage textul de help direct din docstring-uri și din metadatele
Annotated. Se asociază nativ curichpentru culoare și tabele.
# typer + rich, asocierea tipica din 2026
import typer
from rich.console import Console
app = typer.Typer(rich_markup_mode="rich")
console = Console()
@app.command()
def report(name: str) -> None:
"""Print a [bold green]styled[/bold green] greeting."""
console.print(f"Hello, [cyan]{name}[/cyan]!")
rich_markup_mode="rich" transformă tag-urile din docstring în output colorat. Biblioteca rich în sine îți dă tabele, bare de progres, evidențiere de sintaxă și stack trace-uri. Pentru orice CLI la care se va uita un om, combinația typer + rich e implicitul curent.
Notă despre asistența AI. Asistenții de cod sunt neobișnuit de buni la a schela CLI-uri noi. Pattern-urile sunt atât de structurate, declară flag-uri, parsează, dispatch, încât unul-două prompt-uri produc o unealtă funcțională. Modul de eșec previzibil e contrariul: tind să suprainginerizeze. Ceri o unealtă cu o singură comandă și trei flag-uri și primești înapoi un framework cu patru subcomenzi cu
init,config,validateși un flag--verbosecablat prin fiecare nivel. Citește ce ai primit, șterge ce nu-ți trebuie, păstrează părțile care se potrivesc cu scopul real. CLI-urile sunt ușor de crescut mai târziu; complexitatea preventivă e ce nu poți scoate ușor.
Studiu de caz: cum își structurează uv CLI-ul
uv e package manager-ul Python sprijinit pe Rust care a înlocuit în mare măsură pip și pip-tools pentru proiectele noi în ultimii doi ani. Nu e scris în Python, dar structura comenzilor lui e o referință utilă pentru că aceeași formă funcționează la fel de bine cu click sau typer.
Nivelul de sus e un grup, cu comenzi organizate pe zonă de interes:
uv pip install,uv pip compile,uv pip sync— operațiuni compatibile cu pip sub un subgruppip.uv add,uv remove,uv lock,uv sync— operațiuni la nivel de proiect, la nivelul de sus.uv run,uv tool run— execuție.uv python install,uv python list— gestionarea interpretorului.
Lecția nu sunt comenzile specifice; e gruparea. Subcomenzile se aglomerează pe zone de interes, iar operațiunile cele mai obișnuite trăiesc la nivelul de sus pentru mai puține apăsări de tastă. În typer asta se mapează la:
app = typer.Typer()
pip_app = typer.Typer()
python_app = typer.Typer()
tool_app = typer.Typer()
app.add_typer(pip_app, name="pip")
app.add_typer(python_app, name="python")
app.add_typer(tool_app, name="tool")
@app.command()
def add(package: str) -> None: ...
@app.command()
def sync() -> None: ...
@pip_app.command("install")
def pip_install(package: str) -> None: ...
@pip_app.command("compile")
def pip_compile(spec: str) -> None: ...
Apelul add_typer montează o sub-aplicație sub un nume. Din perspectiva utilizatorului, mytool pip install foo e o comandă; din perspectiva codului, e o funcție într-o sub-aplicație.
Împachetare ca script de consolă
Ultimul pas. Acum unealta ta rulează ca python mytool.py args. CLI-urile reale rulează ca mytool args. Trucul e punctul de intrare console_scripts.
În pyproject.toml:
[project]
name = "mytool"
version = "0.1.0"
dependencies = ["typer", "rich"]
[project.scripts]
mytool = "mytool.cli:app"
Linia mytool = "mytool.cli:app" zice „creează un executabil numit mytool care apelează app din modulul mytool.cli”. app aici e obiectul aplicație typer (sau, pentru click, funcția grup). La instalare, pip install . (sau uv pip install ., sau uv tool install . pentru o instalare globală) generează un mic script de lansare în directorul bin al mediului tău, iar mytool devine o comandă reală.
Pentru un proiect destinat să fie instalat global fără a-l adăuga în mediul utilizatorului, uv tool install (sau mai vechiul pipx install) e pattern-ul potrivit, configurează un venv izolat per unealtă și expune executabilul pe $PATH.
Un șablon de plecare rezonabil
Pentru un CLI nou astăzi, scheletul implicit arată așa:
# src/mytool/cli.py
from typing import Annotated
import logging
import typer
from rich.console import Console
from rich.logging import RichHandler
app = typer.Typer(no_args_is_help=True, rich_markup_mode="rich")
console = Console()
def _setup_logging(verbose: bool) -> None:
logging.basicConfig(
level=logging.DEBUG if verbose else logging.INFO,
format="%(message)s",
handlers=[RichHandler(console=console, rich_tracebacks=True)],
)
@app.callback()
def main(
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="enable debug logging")] = False,
) -> None:
"""Mytool: do things."""
_setup_logging(verbose)
@app.command()
def serve(
port: Annotated[int, typer.Option(help="port to bind")] = 8000,
) -> None:
"""Run the server."""
logging.getLogger(__name__).info("serving on port %d", port)
@app.command()
def status() -> None:
"""Show current status."""
console.print("[green]everything is fine[/green]")
if __name__ == "__main__":
app()
Ce-ți dă asta:
- O aplicație typer cu două subcomenzi.
- Un flag
--verbosela nivelul de sus tratat în@app.callback(), care rulează înainte de orice subcomandă. - Logging via
RichHandlerașa că liniile de log și stack trace-urile sunt colorate și aliniate. no_args_is_help=Trueașa că rulareamytoolfără nimic afișează help-ul în loc să iasă în tăcere.
Adaugă o intrare [project.scripts], pip install ., și mytool serve --port 9000 funcționează.
Ăsta e pattern-ul modern. argparse e opțiunea cu cele mai puține dependențe, click e calul de povară consacrat, typer e direcția în care merge codul nou. Indiferent pe care o alegi: păstrează suprafața mică, lasă --help să facă munca de documentare, loghează în loc să dai print și livreaz-o ca script de consolă în clipa în care o folosește mai mult de o persoană.
Referințe: argparse — Parser for command-line options, click documentation, typer documentation, rich documentation, Python Packaging User Guide — Entry points, uv. Consultat 2026-05-01.