Astral's uv is a Rust-based project and package manager that uses
pyproject.toml as its central configuration file. When you run commands like
uv init, uv sync or uv run, uv will:
- Look for a
pyproject.tomlin the project root and keep a lockfile (uv.lock) in sync with it. - Create a virtual environment (
.venv) if one does not already exist. - Read dependency specifications (and any build-system directives) to install or update packages accordingly. (Astral Docs1, RidgeRun.ai2)
In other words, your pyproject.toml drives everything—from metadata to
dependencies to build instructions—without needing requirements.txt or a
separate setup.py file. (Level Up Coding3, Python Packaging4)
The [project] table is defined by PEP 621 and is now the canonical place to
declare metadata (name, version, authors, etc.) and runtime dependencies. At
minimum, PEP 621 requires:
nameversion
However, you almost always want to include at least the following additional fields for clarity and compatibility:
[project]
name = "my_project" # Project name (PEP 621 requirement)
version = "0.1.0" # Initial semantic version
description = "A brief overview" # Short summary
readme = "README.md" # Path to your README file (automatically included)
requires-python = ">=3.10" # Restrict Python versions, if needed
license = { text = "MIT" } # SPDX-compatible license expression or file
authors = [
{ name = "Alice Example", email = "alice@example.org" }
]
keywords = ["uv", "astral", "example"] # (Optional) for metadata registries
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
dependencies = [
"requests>=2.25", # Runtime dependency
"numpy>=1.23"
]nameandversion: Mandatory per PEP 621. (Python Packaging4, Reddit5)descriptionandreadme: Although not mandatory, they help with indexing and packaging tools;readme = "README.md"tellsuv(and PyPI) to include your README as the long description. (Astral Docs1, Python Packaging4)requires-python: Constrains which Python interpreters your package supports (e.g.>=3.10). (Python Packaging4, Reddit5)license: Specify a licence as an SPDX identifier (vialicense = { text = "ISC" }) or point to a file (e.g.license = { file = "LICENSE" }). (Python Packaging4, Reddit5)authors: A list of tables withnameandemail. Many registries (e.g., PyPI) pull this for display. (Python Packaging4, Reddit5)keywordsandclassifiers: These help search engines and package indexes. Classifiers must follow the exact trove list defined by PyPA. (Python Packaging4, Reddit5)dependencies: A list of PEP 508-style requirements (e.g.,"requests>=2.25").uv syncwill install exactly those versions, updating the lockfile as needed. (Astral Docs1, RidgeRun.ai2)
Modern projects typically distinguish between "production" dependencies (those
needed at runtime) and "development" dependencies (linters, test frameworks,
etc.). In PEP 621, you use [project.optional-dependencies] for this:
[project.optional-dependencies]
dev = [
"pytest>=7.0", # Testing framework
"black", # Code formatter
"flake8>=4.0" # Linter
]
docs = [
"sphinx>=5.0", # Documentation builder
"sphinx-rtd-theme"
][project.optional-dependencies]: Each table key (e.g.dev,docs) defines a "dependency group." You can install a group viauv add --group devoruv sync --include dev. (Python Packaging4, DevsJC6)- Why use groups? You keep the lockfile deterministic (via
uv.lock) while still separating concerns (test‐only vs. production). (Medium7, DevsJC6)
If you want to expose command-line interfaces (CLIs) or GUIs through your
package, PEP 621 provides the [project.scripts] and [project.gui-scripts]
tables:
[project.scripts]
mycli = "my_project.cli:main"
[project.gui-scripts]
mygui = "my_project.gui:start"[project.scripts]: Defines console scripts. When you runuv run mycli,uvwill invoke themainfunction inmy_project/cli.py. (Astral Docs8)[project.gui-scripts]: On Windows,uvwill wrap these in a GUI executable; on Unix-like systems, they behave like normal console scripts. (Astral Docs8)- Plugin Entry Points: If your project supports plugins, use
[project.entry-points.'group.name']to register them. (Astral Docs8)
PEP 517/518 require a [build-system] table to tell tools how to build and
install your project. A "modern" convention is to specify setuptools>=61.0
(for editable installs without setup.py) or a lighter alternative like
flit_core. Below is the typical setup using setuptools:
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"requires: A list of packages needed at build time. For editable installs inuv, you need at leastsetuptools>=61.0andwheel. (Python Packaging4, Astral Docs8)build-backend: The entry point for your build backend.setuptools.build_metais the PEP 517-compliant backend for setuptools. (Python Packaging4, Astral Docs8)- Note: If you omit
[build-system],uvwill assumesetuptools.build_meta:__legacy__and still install dependencies, but it won't editably install your own project unless you settool.uv.package = true(see next section). (Astral Docs8)
Astral uv allows you to inject its own settings in [tool.uv]. The most
common option is:
[tool.uv]
package = truetool.uv.package = true: Forcesuvto build and install your project into its virtual environment every time you runuv syncoruv run. Without this,uvonly installs dependencies (not your own package) if[build-system]is missing. (Astral Docs8)- You may also set other
uv-specific keys (e.g., custom indexes, resolver policies) under[tool.uv], butpackageis the most common. (Python Packaging4, Astral Docs8)
Below is a complete example that demonstrates all sections. Adjust values as needed for your own project.
[project]
name = "my_project"
version = "0.1.0"
description = "An illustrative example for Astral uv"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [
{ name = "Alice Example", email = "alice@example.org" }
]
keywords = ["astral", "uv", "pyproject", "example"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
dependencies = [
"requests>=2.25",
"numpy>=1.23"
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black",
"flake8>=4.0"
]
docs = [
"sphinx>=5.0",
"sphinx-rtd-theme"
]
[project.scripts]
mycli = "my_project.cli:main"
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.uv]
package = trueExplanation of key points:
-
Metadata under
[project]:name,version(mandatory per PEP 621) (Python Packaging4, Reddit5)description,readme,requires-python: provide clarity about the project and help tools like PyPI. (Python Packaging4, Reddit5)license,authors,keywords,classifiers: standardized metadata, which improves discoverability. (Python Packaging4, Reddit5)dependencies: runtime requirements, expressed in PEP 508 syntax. (Astral Docs1, RidgeRun.ai2)
-
Optional Dependencies (
[project.optional-dependencies]): -
Entry Points (
[project.scripts]):- Defines a console command
myclithat maps tomy_project/cli.py:main. Invokinguv run mycliwill run themain()function. (Astral Docs8)
- Defines a console command
-
Build System:
setuptools>=61.0pluswheelensures both legacy and editable installs work. ✱ Newer versions of setuptools support PEP 660 editable installs without asetup.pystub. (Python Packaging4, Astral Docs8)build-backend = "setuptools.build_meta"tellsuvhow to compile your package. (Python Packaging4, Astral Docs8)
-
[tool.uv]:package = trueensures thatuv syncwill build and install your own project (in editable mode) every time dependencies change. Otherwise,uvtreats your project as a collection of scripts only (no package). (Astral Docs8)
-
Keep
pyproject.tomlHuman-Readable: Edit it by hand when possible. Modern editors (VS Code, PyCharm) offer TOML syntax highlighting and PEP 621 autocompletion. (Python Packaging4) -
Lockfile Discipline: After modifying
dependenciesor any[project]fields, always runuv sync(oruv lock) to updateuv.lock. This guarantees reproducible environments. (Astral Docs1) -
Semantic Versioning: Follow semver for
versionvalues (e.g.,1.2.3). Bump patch versions for bug fixes, minor for backward-compatible changes, and major for breaking changes. (Python Packaging4) -
Keep Build Constraints Minimal: If you don't need editable installs, you can omit
[build-system](but thenuvwon't build your package; it will only install dependencies). To override, settool.uv.package = true. (Astral Docs8) -
Use Exact or Bounded Ranges for Dependencies: Rather than
requests, userequests>=2.25, <3.0to avoid unexpected major bumps. (DevsJC6) -
Consider Dynamic Fields Sparingly: You can declare fields like
dynamic = ["version"]if your version is computed at build time (e.g. viasetuptools_scm). If you do so, ensure your build backend supports dynamic metadata. (Python Packaging4)
A "modern" pyproject.toml for an Astral uv project should:
- Use the PEP 621
[project]table for metadata anddependencies. - Distinguish optional dependencies under
[project.optional-dependencies]. - Define any CLI or GUI entry points under
[project.scripts]or[project.gui-scripts]. - Declare a PEP 517
[build-system](e.g.setuptools>=61.0,wheel,setuptools.build_meta) to support editable installs, or omit it and rely ontool.uv.package = true. - Include a
[tool.uv]section, at minimumpackage = trueif you wantuvto build and install your own package.
Following these conventions ensures that your project is fully PEP-compliant,
easy to maintain, and integrates seamlessly with Astral uv.
Footnotes
-
UV Tutorial: A Fast Python Package and Project Manager ↩ ↩2 ↩3
-
Writing your pyproject.toml – Python Packaging User Guide ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20
-
Anyone used UV package manager in production? (Reddit) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
The Complete Guide to pyproject.toml – devsjc blogs ↩ ↩2 ↩3 ↩4
-
Start Using UV Python Package Manager for Better Dependency Management ↩
-
Configuring projects | uv - Astral Docs ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13