Python, de la zero Lecția 15 / 60

uv: schimbarea de ecosistem din 2026

De ce uv a inlocuit pip+venv+pyenv+poetry pentru multa lume, ce face diferit si fluxul de lucru.

Dacă începeai un proiect Python în 2020, centura ta de unelte arăta cam așa: pyenv ca să instalezi versiuni de Python, venv ca să faci medii virtuale, pip ca să instalezi pachete, pip-tools (sau Poetry) ca să le blochezi și setuptools ca să construiești wheel-uri. Cinci unelte, cinci fișiere de configurare, cinci locuri unde puteau merge lucrurile prost.

În 2026, tot mai mulți oameni folosesc, pur și simplu, uv.

Ce este uv

uv e un manager de pachete și de proiecte Python de la Astral, aceeași companie din spatele Ruff. A fost lansat la începutul lui 2024 ca un înlocuitor mai rapid pentru pip, apoi în următorul an a crescut tăcut până a putut înlocui întreaga stivă:

  • crearea mediului virtual (înlocuiește venv, virtualenv)
  • instalarea pachetelor (înlocuiește pip)
  • rezolvarea și blocarea dependențelor (înlocuiește pip-tools, părți din Poetry)
  • inițializarea și gestionarea proiectului (înlocuiește părți din Poetry, PDM, Hatch)
  • gestionarea interpretorului Python (înlocuiește pyenv)
  • integrarea cu build backend-ul (funcționează cu hatchling, setuptools etc.)

E scris în Rust, distribuit ca un singur binar static și, asta nu e marketing gol, autentic de 10 până la 100 de ori mai rapid decât pip pentru majoritatea operațiilor. Demo-urile cu „instalează o sută de pachete în două secunde” sunt reale. Până la mijlocul lui 2025 devenise recomandarea implicită în majoritatea tutorialelor Python, iar în 2026 e ce aș lua eu pentru un proiect nou.

Instalarea

Nu faci pip install uv (poți, dar n-ar trebui: uv e o unealtă, nu un pachet Python). Instalează-l ca binar de sistem:

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

# or via your package manager
brew install uv

Atât. Nu e nevoie de Python ca să instalezi uv în sine, ceea ce e drăguț, fiindcă uv poate apoi să-ți instaleze Python ție.

Fluxul „vreau pur și simplu un proiect”

Asta e calea pe care o vei lua în 90% din cazuri:

uv init weather-cli
cd weather-cli

Asta îți dă un director populat:

weather-cli/
├── .python-version
├── .gitignore
├── README.md
├── main.py
└── pyproject.toml

pyproject.toml e deja completat cu valori implicite rezonabile. Adaugi o dependență:

uv add httpx

Trei lucruri se întâmplă instant:

  1. httpx (și dependențele lui) sunt adăugate în [project.dependencies] în pyproject.toml.
  2. Un .venv/ e creat dacă nu exista, iar pachetele sunt instalate în el.
  3. Un fișier uv.lock e generat, fixând fiecare pachet și dependență tranzitivă la o versiune exactă, cu hash-uri.

Rulează codul:

uv run python main.py

uv run se asigură că venv-ul e sincronizat cu lockfile-ul înainte de a rula, așa că nu poți ajunge niciodată în starea „am uitat să instalez ceva?”. Dacă pyproject.toml zice că ai nevoie de un pachet care nu e instalat, uv îl instalează. Dacă lockfile-ul e depășit, uv îl actualizează. Nu activezi nimic. Nu te gândești la nimic.

Ca să elimini un pachet: uv remove httpx. Ca să actualizezi tot: uv lock --upgrade apoi uv sync. Ca să instalezi o versiune anume a unui pachet: uv add 'httpx>=0.27,<0.30'.

Lockfile-ul

uv.lock e echivalentul lui poetry.lock sau al requirements.txt-ului compilat de pip-tools. Înregistrează versiunea rezolvată exactă a fiecărui pachet, fiecare dependență tranzitivă, fiecare URL și fiecare hash. Comite-l în git. Asta e ce face build-urile reproductibile: oricine clonează repo-ul tău și rulează uv sync va primi pachete identice bit cu bit cu ce ai tu.

pyproject.toml zice ce vrei (httpx>=0.27). uv.lock zice ce ai primit (httpx==0.27.2, plus cele 14 pachete tranzitive pe care le-a adus cu el). Când deployezi, instalezi din lock, nu din declarațiile lejere. Asta e singura cale să eviți „dar mergea în dev.”

Modul compatibil cu pip

Dacă ai un proiect existent și nu vrei să-ți schimbi fluxul, uv are un înlocuitor pip drop-in:

uv pip install requests
uv pip install -r requirements.txt
uv pip freeze > requirements.txt
uv venv  # create a venv (replaces python -m venv)

Flag-urile și comportamentul oglindesc pe cele ale lui pip, doar că mult mai rapid. Ăsta a fost modul original al lui uv la lansare și e încă util pentru proiecte moștenite, scripturi CI și orice deja vorbește „pip”. Dar pentru proiecte noi, preferă modul de proiect (uv add, uv run): acolo strălucește uv.

Gestionarea interpretorului Python

Asta e partea care m-a surprins prima dată când am folosit-o. uv poate instala el însuși Python.

uv python install 3.13
uv python list

Sunt build-uri Python reale (din proiectul python-build-standalone), instalate în cache-ul lui uv. Când creezi un proiect, uv citește requires-python din pyproject.toml și fie folosește un interpretor instalat care se potrivește, fie descarcă unul automat.

# pyproject.toml
[project]
requires-python = ">=3.13"

Dacă 3.13 nu e instalat, uv sync îl aduce. Pasul pyenv din vechiul flux pur și simplu dispare. (Poți încă folosi Python-ul de sistem sau cele instalate de pyenv dacă vrei: uv le verifică pe toate.)

Există și un fișier .python-version (creat de uv init) care fixează proiectul la o versiune anume, ca fiecare colaborator să primească același interpretor fără coordonare.

Migrarea de la alte unelte

De la pip + venv + requirements.txt:

cd existing-project
uv init --no-readme --no-pin-python   # creates pyproject.toml, doesn't overwrite
uv add -r requirements.txt            # imports your existing pins

Acum ai un pyproject.toml și un uv.lock. Șterge requirements.txt când ești convins.

De la Poetry: uv citește parțial pyproject.toml în stil Poetry, dar calea cea mai curată e uvx migrate-to-uv (o unealtă de conversie one-shot). Rescrie secțiunile [tool.poetry] în formă PEP 621 [project] și generează un uv.lock din poetry.lock-ul tău.

De la conda: uv nu înlocuiește conda pentru stiva științifică cu dependențe C/Fortran: aia rămâne forța condei. Dar pentru proiecte pure-Python, de obicei poți schimba conda cu uv și obții medii mai rapide și mai ușoare. Pentru proiecte mixte, pixi (tot Rust, tot rapid) e echivalentul de formă conda.

Un parcurs pentru un proiect nou

Hai să pornim unul de la zero, cap-coadă. Imaginează-ți un script ETL mic care extrage JSON dintr-un API și scrie Parquet.

uv init etl-job --python 3.13
cd etl-job

Adaugă dependențele:

uv add httpx polars
uv add --dev pytest ruff mypy

--dev le adaugă într-un grup de dependențe dev (PEP 735) în loc de dependencies principale. Sunt instalate în mediul tău local, dar nu pleacă cu pachetul.

Uită-te la pyproject.toml:

[project]
name = "etl-job"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    "httpx>=0.27.2",
    "polars>=1.5.0",
]

[dependency-groups]
dev = [
    "pytest>=8.3",
    "ruff>=0.5",
    "mypy>=1.11",
]

Scrie un test:

# tests/test_etl.py
def test_arithmetic():
    assert 2 + 2 == 4

Rulează-l:

uv run pytest

uv se asigură că pytest și dependențele lui sunt instalate (sunt: stau în grupul dev, care e instalat implicit), apoi le rulează în venv-ul proiectului. Același pattern pentru orice:

uv run ruff check .
uv run mypy src/
uv run python -m etl_job

Ai nevoie de o unealtă one-off care nu face parte din proiectul tău? Folosește uvx (alias pentru uv tool run):

uvx black .                  # run black without installing it persistently
uvx --from cookiecutter cookiecutter gh:audreyr/cookiecutter-pypackage

uvx rulează unelte efemere și izolate, ca pipx run, dar instantaneu fiindcă uv pune totul în cache. Îl folosesc de zeci de ori pe zi pentru lucruri precum uvx ruff format, uvx httpie, uvx ipython.

De ce e atât de rapid

Câteva motive contează:

  • Descărcări paralele. pip descarcă pachetele serial; uv descarcă în paralel și începe să extragă wheel-uri imediat ce sosesc bytes.
  • Resolver mai inteligent. uv folosește PubGrub, același algoritm pe care îl folosesc Cargo și pub-ul lui Dart. Face backtracking inteligent și dă mesaje de eroare mult mai bune când o rezolvare eșuează.
  • Cache global cu hard link-uri. uv ține o singură copie a fiecărei versiuni de pachet pe disc și face hard-link în venv-urile proiectelor. Crearea unui venv proaspăt cu 50 de pachete durează sute de milisecunde fiindcă nimic nu e copiat efectiv.
  • Fără cost de pornire Python pe apel. pip e un program Python, deci fiecare invocare plătește taxa de pornire a Python-ului. uv e un binar nativ; pornește în câteva milisecunde.

Asta o simți cel mai mult la instalări la rece și pe CI. Un pip install -r requirements.txt tipic pentru un proiect cu 50 de dependențe poate dura 30-60 de secunde. Aceeași cu uv sync e adesea sub două secunde.

CI cu uv

Povestea CI e locul unde diferența de viteză se compunde. Un job tipic GitHub Actions:

- uses: astral-sh/setup-uv@v3
  with:
    enable-cache: true
- run: uv sync --frozen
- run: uv run pytest
- run: uv run ruff check .

--frozen îi spune lui uv să refuze să actualizeze lockfile-ul: dacă pyproject.toml și uv.lock sunt nesincronizate, build-ul eșuează în loc să rezolve tăcut. Asta vrei pe CI: build-urile fie se potrivesc exact cu lockfile-ul comis, fie nu rulează. enable-cache: true păstrează cache-ul de pachete al lui uv între rulări, așa că al doilea push instalează în milisecunde.

Aici se plătește și trucul cu hard link-urile al lui uv: chiar și pe un cache rece, descărcarea și rezolvarea a 50 de pachete pe un runner CI se termină de obicei înainte ca prima linie de log a acțiunii să iasă din ecran.

Ce nu face

  • Nu e un build backend. Folosește hatchling (sau favoritul tău). uv apelează backend-ul prin PEP 517.
  • Nu e o unealtă de publish… ei bine, în mare parte nu. uv publish există în 2026 și încarcă pe PyPI, dar fluxul e în continuare „construiește wheel-ul, apoi încarcă-l.”
  • Nu gestionează dependențe ne-Python. Dacă ai nevoie de libpq sau ffmpeg instalate la nivel de sistem, uv nu te ajută. (Pentru asta, uită-te la pixi sau la package manager-ul OS-ului tău.)
  • Nu înlocuiește magia de detecție Python a IDE-ului tău. VS Code, PyCharm etc. trebuie îndreptate către .venv/ (sau calea sa absolută) ca pentru orice altă unealtă. Majoritatea editoarelor auto-detectează un director .venv la rădăcina proiectului, ceea ce e exact ce creează uv.

Lucruri de știut care nu sunt în docs

Câteva detalii care m-au mușcat înainte să le învăț:

  • uv sync șterge pachetele care nu sunt în lockfile. Ăsta e comportamentul corect: ține venv-ul consistent cu ce e urmărit, dar dacă faci uv pip install something-experimental ca să încerci ceva, următorul uv sync îl smulge. Ca să-l păstrezi, rulează uv add în schimb.
  • uv run întotdeauna re-sincronizează prima dată dacă nu treci --no-sync. Dacă ai un check pre-flight lung sau o rezolvare lentă a lockfile-ului, asta adaugă latență. Pentru o buclă internă strânsă (de exemplu rularea repetată a unui singur test), --no-sync e prietenul tău.
  • Versiunile uneltelor sunt independente. uvx ruff rulează cel mai recent ruff. uv run ruff rulează versiunea fixată în dependențele de dev ale proiectului tău. Sunt comenzi diferite și pot să nu fie de acord. Pentru lint legat de proiect, preferă uv run ca toți să primească aceeași versiune.

Concluzia

Cinci unelte prăbușite într-una. Fluxul e uv init, uv add, uv run, uv sync. Suprasarcina mentală a dispărut. Dacă pornești un proiect Python nou în 2026, asta e calea celei mai mici rezistențe și cea pe care a convergit majoritatea ecosistemului.

Acum că poți instala lucruri și le poți bloca, lecția următoare e despre unde să le pui: cum ar trebui să fie organizat un proiect Python, src/ versus flat, unde stau testele și scripturile și convențiile care s-au sedimentat până în 2026.

Caută