Use uv for Python dependency management#
Status#
Accepted
Context#
Since 2018, OEP-18: Python Dependency Management has defined the standard workflow for Python dependencies in Open edX repositories:
Direct dependencies declared in
requirements/*.infilespip-compile(from pip-tools) generates pinnedrequirements/*.txtlockfilespip-syncinstalls exactly those pinned versions into a virtualenvmake upgradeorchestrates the whole process
This workflow has served the project well, but the Python packaging ecosystem has evolved significantly:
PEP 621 (2021) standardized project metadata in
pyproject.toml, makingsetup.pyandsetup.cfglegacy.PEP 735 (2024) added dependency groups to
pyproject.toml, providing a standard way to declare context-specific dependencies (test, docs, CI, etc.) — the same problem OEP-18 solved with multiple.infiles.uv emerged as a fast, unified tool that handles dependency resolution, locking, virtual environment management, and tool execution in a single binary. It natively understands
pyproject.tomland PEP 735 dependency groups.
Meanwhile, pip-tools has not added support for PEP 735 dependency groups,
and continuing to use it means maintaining a parallel dependency declaration
system (requirements/*.in) alongside pyproject.toml. Additionally,
Jazzband — the organization that maintains pip-tools — announced in
March 2026 that it is sunsetting, leaving the long-term governance and
maintenance of pip-tools uncertain.
Decision/Consequence#
Open edX Python repositories should use uv for dependency management:
Declare dependencies in
pyproject.tomlusing PEP 621 metadata and PEP 735 dependency groups. This replacesrequirements/*.infiles.Lock dependencies with
uv lock, producing a singleuv.lockfile. This replaces multiplerequirements/*.txtfiles generated bypip-compile.Install dependencies with
uv sync, which creates and manages the virtual environment. This replacespip-syncand manualpython -m venvsteps.Run tools with
uv runwhen a tool is installed in the project’s virtualenv (e.g.,uv run tox,uv run pytest).Preserve the
make upgradepattern — the target now runsuv lock --upgradeinstead ofpip-compile --upgrade.
For test matrices that need multiple versions of a dependency (e.g., Django),
use [tool.uv].conflicts with separate dependency groups and
uv-venv-lock-runner in tox. This ensures fully reproducible, locked
resolutions for each matrix combination.
Shared constraints (e.g., the global constraints file from edx-lint) are
written into [tool.uv].constraint-dependencies in pyproject.toml via
the edx_lint write_uv_constraints command, since uv lock does not
accept a --constraint CLI flag.
Rejected Alternatives#
pip-tools (status quo)
pip-tools is mature and well-understood in the Open edX community. However, it does not support PEP 735 dependency groups, requires a separate tool for virtual environment management, and is significantly slower at dependency resolution than uv. Continuing to use it means maintaining
requirements/*.infiles alongsidepyproject.toml, duplicating dependency declarations. The sunsetting of Jazzband also raises questions about long-term maintenance.Poetry
Poetry is a popular all-in-one tool, but it is opinionated about project structure, uses a non-standard lock format (
poetry.lock), and has limited support for the kind of dependency groups needed for Open edX code test matrices (e.g., multiple Django versions). Its resolver has historically been slower than uv’s.PDM
PDM supports PEP 735 and produces a standard lock format, but has a smaller community and less ecosystem adoption. uv has gained broader momentum in the Python packaging space and is backed by the same team (Astral) that maintains
ruff, which is already widely adopted in the broader Python community.
Examples#
The following examples are drawn from openedx/sample-plugin and illustrate the key patterns. See that repository for a complete working reference.
Example pyproject.toml#
[build-system]
requires = ["setuptools", "setuptools-scm>8.1"]
build-backend = "setuptools.build_meta"
[project]
name = "openedx-sample-plugin"
description = "A sample backend plugin for the Open edX Platform"
requires-python = ">=3.12"
license = "Apache-2.0"
dependencies = [
"Django",
"djangorestframework",
"openedx-events",
]
# -- Dependency groups (PEP 735) ------------------------------------------
#
# These replace the old requirements/*.in files. Each group maps to a usage
# context (testing, quality checks, docs, CI tooling, local dev).
[dependency-groups]
# Framework-agnostic test deps. Not used directly — included by the
# version-specific groups below.
test-base = [
"pytest-cov",
"pytest-django",
]
# Current default Django version. Used by quality, docs, and as the default
# test matrix entry.
test = [
{include-group = "test-base"},
"Django>=5.0,<6.0",
]
# Additional Django versions under test. Each gets its own group so uv can
# produce a separate locked resolution via [tool.uv].conflicts.
django60 = [
{include-group = "test-base"},
"Django>=6.0,<7.0",
]
quality = [
{include-group = "test"},
"edx-lint",
"pycodestyle",
]
doc = [
{include-group = "test"},
"Sphinx",
"doc8",
]
ci = [
"tox",
"tox-uv",
]
dev = [
{include-group = "quality"},
{include-group = "ci"},
]
# -- uv configuration -----------------------------------------------------
[tool.uv]
# Mutually exclusive groups: uv produces one locked resolution per entry.
conflicts = [
[{group = "test"}, {group = "django60"}],
]
# DO NOT EDIT constraint-dependencies DIRECTLY.
# This list is managed by `edx_lint write_uv_constraints`
# and will be overwritten the next time `make upgrade` is run.
# - GLOBAL constraints: edit edx_lint/files/common_constraints.txt
# - REPO-SPECIFIC constraints: edit constraints.txt next to pyproject.toml
constraint-dependencies = [
"Django<7.0",
"elasticsearch<7.14.0",
]
Example tox.ini#
[tox]
envlist = py312-django{52,60},quality,docs
requires =
tox-uv>=1
[testenv]
runner = uv-venv-lock-runner
dependency_groups =
django52: test
django60: django60
commands =
pytest {posargs}
[testenv:quality]
dependency_groups =
quality
commands =
pylint my_package tests
[testenv:docs]
dependency_groups =
doc
commands =
doc8 docs
make -e -C docs html
Common commands#
# Create a virtual environment
uv venv .venv --seed --python 3.12
# Install all dev dependencies (creates .venv if needed)
uv sync --group dev
# Run tests via tox
uv run tox
# Run a single tox environment
uv run tox -e py312-django60
# Upgrade all locked dependencies (the new "make upgrade")
uv lock --upgrade
# Sync shared constraints from edx-lint into pyproject.toml
uv run --with edx-lint edx_lint write_uv_constraints
# Full upgrade sequence (typically wrapped in a Makefile target)
uv run --with edx-lint edx_lint write_uv_constraints
uv lock --upgrade
References#
PEP 621 — Storing project metadata in pyproject.toml
PEP 735 — Dependency Groups in pyproject.toml
uv documentation
Jazzband and their sunsetting announcement
openedx/sample-plugin — reference implementation
OEP-18: Python Dependency Management — the predecessor workflow this decision replaces
Change History#
2026-04-24#
Document created