Dacă deschideai un proiect Python acum cinci ani, te întâmpina o mică grădină zoologică veselă: setup.py, setup.cfg, MANIFEST.in, requirements.txt, requirements-dev.txt, poate un tox.ini și, dacă mainerul era deosebit de meticulos, un .flake8 și un pytest.ini pe deasupra. Fiecare avea formatul lui, ciudățeniile lui și opiniile lui despre cum se scrie „dependență”.
În 2026 ai nevoie, în mare, de un singur fișier: pyproject.toml. Lecția asta e turul.
O scurtă istorie (ca prezentul să aibă sens)
Unealta originală de packaging din Python era setuptools, configurată printr-un script setup.py. Problema e chiar în nume: e un script. Ca să afli orice despre un pachet (numele, dependențele, versiunea), trebuia să execuți cod Python arbitrar. E și o bătaie de cap de securitate, și un coșmar pentru introspecție. Vrei o unealtă care citește metadatele unui proiect? Sperăm că nimeni nu a pus import requests în capul lui setup.py.
setup.cfg a apărut ca un adaos declarativ: aceleași metadate, dar în format INI, ca uneltele să le poată parsa fără să ruleze nimic. Mai bine, dar acum aveai două fișiere care descriau același lucru, iar setup.py era încă necesar ca stub.
Apoi au venit două PEP-uri care au reparat totul în liniște:
- PEP 518 (2016) a introdus
pyproject.tomlși tabelul[build-system]. În sfârșit, o cale de a spune „proiectul ăsta se construiește cu X” fără să ruleze cod Python înainte. - PEP 621 (2020) a adăugat tabelul
[project]: un loc standard, agnostic față de unelte, pentru metadate: nume, versiune, dependențe, autori, tot tacâmul.
Până în 2024, fiecare build backend modern suporta PEP 621, iar setup.py a alunecat tăcut în categoria moștenirilor. Îl mai vezi în proiecte mai vechi (și încă poți scrie unul, dacă chiar vrei), dar pentru orice proiect nou în 2026, răspunsul este pyproject.toml.
Cele trei tipuri de tabele
Un pyproject.toml are, în mare, trei tipuri de secțiuni:
[build-system]: cum se construiește pachetul. Obligatoriu dacă publici.[project]: metadatele standardizate despre pachetul tău.[tool.*]: configurare specifică uneltei. Orice e subtool.poate fi revendicat de o unealtă individuală. Ruff stă la[tool.ruff], pytest la[tool.pytest.ini_options], mypy la[tool.mypy]și așa mai departe.
Hai să parcurgem un fișier realist.
Un pyproject.toml aproape real
[build-system]
requires = ["hatchling>=1.18"]
build-backend = "hatchling.build"
[project]
name = "weather-cli"
version = "0.3.1"
description = "A tiny CLI that fetches the weather for a city."
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
authors = [
{ name = "Narcis Miclaus", email = "hello@example.com" }
]
keywords = ["weather", "cli", "openmeteo"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"httpx>=0.27",
"click>=8.1",
"rich>=13.7",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov",
"ruff>=0.4",
"mypy>=1.10",
]
[project.scripts]
weather = "weather_cli.__main__:main"
[project.urls]
Homepage = "https://github.com/example/weather-cli"
Issues = "https://github.com/example/weather-cli/issues"
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
[tool.mypy]
python_version = "3.11"
strict = true
Secțiune cu secțiune:
[build-system]. Spune „ca să mă construiești, instalează hatchling și apelează hatchling.build.” Frontend-urile de build precum pip, uv și build citesc asta și se conformează. Nu vei invoca niciodată backend-ul direct.
[project]. Lucrurile bune. name și version sunt obligatorii. requires-python contează mai mult decât ai crede: împiedică utilizatorii pe interpretori mai vechi să instaleze din greșeală un pachet care nu va merge și spune uneltelor precum uv ce Python să aducă.
dependencies este o listă de string-uri PEP 508. Formatul clasic: package>=1.2,<2.0; python_version >= "3.10". Poți fixa lejer (httpx>=0.27), strâns (httpx==0.27.2) sau cu markeri (pywin32; sys_platform == "win32").
optional-dependencies sunt extras. pip install weather-cli[dev] instalează grupul de dev. Grupuri obișnuite: dev, test, docs. (Notă: tabelul dependency-groups din PEP 735 câștigă teren în 2026 pentru dependențe de dev nepublicate: aceeași idee, dar nu poluează metadatele wheel-ului tău publicat. Merită cunoscut.)
project.scripts generează puncte de intrare în consolă. După pip install weather-cli, utilizatorul primește o comandă weather în PATH care apelează main() din weather_cli.__main__. Gata cu scripturile shell chmod +x.
project.urls apare pe PyPI ca acele link-uri utile din bara laterală.
[tool.*] sunt acolo unde vor uneltele tale. Ruff, pytest, mypy, coverage, hatch: toate citesc acum din pyproject.toml. Un fișier. O singură sursă de adevăr.
Build backends: cine construiește efectiv wheel-ul?
Tabelul [build-system] alege un build backend. Sunt patru pe care îi vei întâlni în sălbăticie:
hatchling: din proiectul Hatch. Rapid, simplu, valori implicite sănătoase, fără magie. În 2026 e alegerea recomandată pentru începători și ce alege uv init pentru tine. Dacă pornești de la zero, folosește-l.
setuptools: garda veche. Încă omniprezent fiindcă majoritatea pachetelor existente îl folosesc. Funcționează bine, dar suprafața de configurare e uriașă și plină de ciudățenii moștenite (de exemplu package_dir, find_packages, ciudățenii cu namespace packages). Alege-l dacă întreții ceva care deja îl folosește.
poetry-core: build backend-ul din spatele Poetry. Părtinitor; presupune că folosești tot fluxul Poetry. Dacă nu te angajezi la Poetry ca manager, alege altceva.
flit-core: minim. Doar pachete pure-Python, fără extensii C, fără pași de build complicați. Dacă pachetul tău e un singur director de fișiere .py, flit e o încântare.
Mai sunt și altele: pdm-backend, maturin pentru extensii Rust, scikit-build-core pentru proiecte C++/CMake, dar aceia patru acoperă cea mai mare parte a muncii pure-Python.
De ce uv recomandă hatchling? Viteză și predictibilitate. Hatchling nu are un mod implicit de „descoperă tot” care să te surprindă la build, configurația lui se mapează aproape 1:1 pe PEP 621 și construiește wheel-uri în milisecunde. Nu există un motiv bun să alegi altceva pentru un proiect nou pure-Python.
Conversia unui setup.py vechi în pyproject.toml
Să zicem că moștenești asta:
# setup.py
from setuptools import setup, find_packages
setup(
name="oldthing",
version="1.2.0",
description="An old thing.",
author="Someone",
author_email="someone@example.com",
packages=find_packages(),
install_requires=[
"requests>=2.28",
"pyyaml",
],
extras_require={
"dev": ["pytest", "black"],
},
entry_points={
"console_scripts": [
"oldthing=oldthing.cli:main",
],
},
python_requires=">=3.9",
)
Traducerea e în mare parte mecanică:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "oldthing"
version = "1.2.0"
description = "An old thing."
requires-python = ">=3.9"
authors = [{ name = "Someone", email = "someone@example.com" }]
dependencies = [
"requests>=2.28",
"pyyaml",
]
[project.optional-dependencies]
dev = ["pytest", "black"]
[project.scripts]
oldthing = "oldthing.cli:main"
Apoi șterge setup.py. Hatchling va descoperi automat pachetele de sub src/ sau din rădăcina proiectului; dacă layout-ul tău e neobișnuit, adaugi un tabel [tool.hatch.build.targets.wheel] ca să-l ghidezi. Rulează python -m build (sau, mai bine, uv build) și vei obține un wheel identic la celălalt capăt.
Două capcane la migrare:
- Versiuni dinamice. Dacă vechiul tău
setup.pycitea versiunea din__init__.pysau dintr-un tag Git, ai nevoie dedynamic = ["version"]în[project]și de un bloc[tool.hatch.version]corespunzător care să-i spună hatchling-ului unde să caute. MANIFEST.in. Vechea metodă de a include fișiere ne-Python în source distributions. Hatchling are propria configurare[tool.hatch.build]pentru asta; de cele mai multe ori, valorile implicite fac deja ce trebuie.
TOML în 30 de secunde
Dacă n-ai mai atins TOML: e un format INI-formă cu tipuri proprii. String-uri în ghilimele duble, array-uri în paranteze drepte, tabele în [paranteze.drepte], sub-tabele sub nume cu puncte. Biblioteca standard Python îl parsează nativ din 3.11 (import tomllib), așa că orice unealtă care vrea să citească configurația ta o poate face fără o dependență suplimentară.
Singura capcană TOML care merită semnalată: array-urile de tabele folosesc paranteze duble:
[[project.authors]]
name = "Alice"
email = "alice@example.com"
[[project.authors]]
name = "Bob"
email = "bob@example.com"
PEP 621 îți permite să scrii asta mai compact, cu inline tables (authors = [{ name = "Alice", email = "..." }, ...]), ceea ce fac majoritatea proiectelor. Ambele forme sunt valide; alege-o pe care se citește mai bine.
Ce nu este pyproject.toml
Nu e un lockfile. Lista dependencies zice „vreau httpx >= 0.27”; nu zice „mai exact httpx 0.27.2 cu aceste dependențe tranzitive precise.” Pentru asta e lockfile-ul, iar package manager-ul tău îl generează separat (uv.lock, poetry.lock, output-ul compilat al lui pip-tools).
Și nu e nici magie. Uneltele trebuie să fie de acord să-l citească. Un linter care nu suportă [tool.<nume>] nu va începe brusc s-o facă fiindcă ai adăugat tabelul. Majoritatea uneltelor Python moderne o fac; unele vechi (mă uit la tine, flake8) au avut nevoie de plugin-uri ca să se pună la curent.
Greșeli frecvente
Câteva lucruri care prind oamenii la primul lor pyproject.toml:
- Pinning prea strâns în biblioteci. Dacă publici o bibliotecă,
dependencies = ["httpx==0.27.2"]e ostil utilizatorilor: oricine fixează o versiune diferită de httpx nu mai poate instala pachetul tău alături. Folosește intervale (>=0.27,<0.28sau doar>=0.27). Pin-urile strânse aparțin proiectelor de aplicație, unde lockfile-ul se ocupă de reproductibilitate. - Uitarea lui
requires-python. Fără el, pachetul tău se instalează pe Python 2.7 și produce erori de sintaxă criptice. Setează mereu o limită inferioară rezonabilă. - Amestec de tab-uri și spații în TOML. TOML e tolerant la spații pentru indentare, dar spațierea mixtă în interiorul array-urilor și inline tables produce erori ciudate de parser („Expected ’=’ after a key in a key/value pair”). Rămâi la spații.
- Comentarii la mijlocul array-ului. TOML permite
# comentariipe linii proprii sau la capătul rândului, dar un comentariu între două elemente de array trebuie să fie pe propria linie:["a", # nu, "b"]parsează, dar înghite al doilea element.
Citirea pyproject.toml din Python
Uneori vrei să citești singur fișierul: ca să afișezi versiunea pachetului, să generezi documentație sau să cablezi un tooling personalizat. Python vine la pachet cu un parser TOML din 3.11:
import tomllib
from pathlib import Path
data = tomllib.loads(Path("pyproject.toml").read_text())
print(data["project"]["name"])
print(data["project"]["dependencies"])
Pentru versiuni Python mai vechi, instalează tomli (aceeași bibliotecă, înglobată în stdlib). De reținut că tomllib este read-only prin design: dacă vrei să scrii TOML, folosește tomli-w sau tomlkit (al doilea păstrează comentariile și formatul, ceea ce contează când actualizezi un fișier existent pe loc).
Un pattern util: citește o singură dată versiunea pachetului, într-un CLI sau aplicație web, și expune-o în output-ul --version. Cu importlib.metadata nu mai trebuie nici măcar să parsezi fișierul:
from importlib.metadata import version
print(version("weather-cli"))
Asta extrage din metadatele pachetului instalat, care au fost generate din pyproject.toml la build. O singură sursă de adevăr, fără parsing.
Unde ne lasă asta
pyproject.toml e lingua franca. Fiecare package manager modern (pip, uv, Poetry, PDM, Hatch) îl citește. Fiecare build backend modern scrie wheel-uri din el. Fiecare linter și type checker modern poate fi configurat în el. Grădina zoologică fragmentată de fișiere de configurare din 2018 s-a prăbușit într-un singur fișier TOML bine specificat.
În lecția următoare ne vom întâlni cu uv: package manager-ul de la Astral care a luat cu asalt ecosistemul în 2024-2025 și care, în 2026, e alegerea implicită pentru majoritatea proiectelor Python noi. Citește pyproject.toml, evident. Totul îl citește acum.