Packaging, Environments and Dependency Management
Build reproducible Python projects with venv, pip, pyproject.toml, uv and Poetry: pin dependencies, lock the graph, run commands and publish a package without surprises
A service passed its tests on Friday and failed on Monday. The build had installed a newer transitive dependency, the continuous integration cache held a different resolver result, and the production image used a different Python minor version. The repository listed the direct dependencies in a text file, but nothing recorded the exact graph that had passed the tests.
Three artifacts prevent that failure. A virtual environment isolates the packages for one project. A declaration file records the direct requirements and the Python version. A lock file records the exact versions and hashes of the whole graph. Together they make an install reproducible on another machine.
This lesson starts with the environment, then the declaration and the lock, then the two modern project managers that automate all three. It ends with building and publishing a distribution, and with a decision guide for teams that already use pip, uv or Poetry.
Key terms
- A directory that contains a Python interpreter and its own site-packages, isolated from the system interpreter and from other projects.
- A package that a project needs in order to run or to be tested, declared with a version constraint.
- A package that the project declares itself, as opposed to one that is pulled in by another package.
- A package required by one of the project's dependencies, resolved automatically and recorded in the lock file.
- The algorithm that chooses a set of package versions that satisfies every constraint at once.
- A generated file that records the exact versions and hashes the resolver selected for the whole dependency graph.
- A built binary distribution with a .whl suffix that pip installs without running a build step.
- A distribution containing source code and build metadata, usually a .tar.gz file, that the installer must build.
- An install that points the environment at the project source tree so code changes take effect without reinstalling.
- A system interpreter that a distribution marks as off limits for pip installs, defined by PEP 668.
- A named set of development-only dependencies in pyproject.toml, standardized by PEP 735.
- A PyPI upload flow that exchanges a short lived OpenID Connect token for permission instead of storing a long lived API token.
Virtual environment
Dependency
Direct dependency
Transitive dependency
Dependency resolver
Lock file
Wheel
Source distribution
Editable install
Externally managed environment
Dependency group
Trusted publishing
1. Why Environments and Locks Exist
A Python interpreter has a search path. When code imports a package, the
interpreter looks through sys.path and loads the first match. A system
interpreter shares one set of installed packages across every project that uses
it. Two projects that need different versions of the same library cannot both
be satisfied in that shared space.
A virtual environment is a directory with its own interpreter entry point and
its own site-packages. Creating one does not copy the whole standard library
or the operating system. It creates a small tree that points back to a base
interpreter and gives that tree a private place to install packages. Deleting
the directory removes the environment without touching the system.
Description
A flowchart showing one system interpreter feeding three isolated environments. The system interpreter provides the base runtime, while project A and project B each get a private site-packages with different versions of the same library, and a tools environment stays separate from both.
The second problem is resolution. A project declares ranges, such as
requests>=2.32,<3, and the resolver chooses one version that satisfies every
range in the graph. That choice depends on the package index, the Python
version and the constraints of every transitive dependency. Re-running the
resolver next month can produce a different answer even when the declaration
did not change.
A lock file freezes one answer. It records the exact version of every package and usually a hash of every artifact. The declaration says what is allowed; the lock says what was tested. Committing the lock makes the test environment and the production environment agree, provided both use the same Python version and the same package index.
A virtual environment isolates packages, not the operating system. It is not a security sandbox, it does not hide the filesystem, and it does not stop code from calling a system library. Use a container or a dedicated user account when the threat model requires real isolation.
A third artifact records the interpreter itself. Tools such as uv write a
.python-version file, and pyproject.toml carries a requires-python range.
The environment is reproducible only when the interpreter version, the
declaration and the lock all agree. A lock created on Python 3.12 may contain
markers that resolve differently on 3.15, which is why continuous integration
should use the same version as production.
2. Creating and Using a Virtual Environment
The standard library includes venv, so no package is needed to create the
first environment. The command takes the directory to create and uses the
interpreter that runs it as the base.
python3 -m venv .venv
source .venv/bin/activate
python -m pip --version
deactivateOn Windows PowerShell the activation script is different, and the rest of the workflow is the same.
python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip --version
deactivateActivation changes the shell's PATH so that python resolves to the
environment's interpreter. It is convenient and optional. The direct path works
in a script, a CI job or a Makefile without changing shell state.
.venv/bin/python -m pip install "httpx>=0.28,<1"
.venv/bin/python -m pytestInside the environment, sys.executable points at the environment's
interpreter and sys.prefix points at the environment directory. Printing both
is the fastest way to confirm which interpreter is running.
import sys
print(sys.executable)
print(sys.prefix)
print(sys.base_prefix)The output depends on the machine, but the shape is stable. In an active
environment, sys.prefix differs from sys.base_prefix. Outside one, the two
values are equal. Use this check in a diagnostic command when a script behaves
differently under a scheduler or an editor's run button.
The environment directory should not be committed. A typical Python
.gitignore includes .venv/, __pycache__/, *.py[cod], build/, dist/
and *.egg-info/. The lock file and pyproject.toml are committed, because
they are the recipe rather than the result.
Never run sudo pip install against the system interpreter. It can overwrite
files owned by the operating system and break tools that depend on them. If
the distribution marks the interpreter as externally managed, pip refuses the
install for the same reason. Create a virtual environment instead.
An environment can be recreated at any time from the declaration and the lock.
Treat it as disposable. When an import fails in a confusing way, delete .venv
and recreate it from the lock before debugging the package itself. That single
step removes a large class of stale state problems.
3. pip, Requirements and Version Specifiers
pip is the installer, not the project manager. It reads a requirement,
resolves a version, downloads a wheel or an sdist and installs it into the
active environment. Running it as python -m pip makes the target interpreter
explicit, which avoids the classic mistake of installing into a different
Python than the one that runs the code.
python -m pip install "requests>=2.32,<3"
python -m pip install -r requirements.txt
python -m pip list --outdated
python -m pip checkA requirements file is a list of constraints, one per line, with comments and options. It can pin exact versions, allow ranges, point at a local path or a Git repository, and include hashes. The full format is larger than most projects need, but the subset below covers ordinary deployment.
# requirements.txt
requests==2.32.5
httpx>=0.28,<1
pandas~=2.3.0
-e .pip freeze writes the exact versions of everything currently installed,
including transitive dependencies. That is useful for reproducing an
environment and misleading as a declaration, because the file no longer
distinguishes what the project asked for from what the resolver added.
pip list --format=freeze produces the same machine readable form.
Version specifiers follow PEP 440, and their punctuation is easy to misread.
Specifier | Meaning |
|---|---|
==2.32.5 | Exactly that version |
==2.32.* | Any version whose release starts with 2.32 |
>=2.32 | That version or newer, with no upper bound |
>=2.32,<3 | At least 2.32 and below 3.0 |
~=2.32.0 | At least 2.32.0 and below 2.33.0 |
!=2.32.5 | Any version except that one |
The compatible release operator is the one that surprises people. ~=2.32.0
allows patch releases but not a new minor version, while ~=2.32 allows
2.32.x and 2.33.x but not 3.0. Choose the narrowest range that the project can
actually support, and let the lock file choose the exact build.
python -m pip freeze > requirements.txt
python -m pip install --require-hashes -r requirements-lock.txtA hash checked install refuses any artifact whose hash is not in the file, which blocks a tampered or unexpectedly republished package. The file must list hashes for every requirement and every transitive dependency, and pip enables hash checking automatically when the first hash appears. Generating that file by hand is painful, which is why pip-tools and uv exist.
Keep a short requirements.in with direct dependencies and a generated
requirements.txt with the full resolved graph. pip-compile writes the
second from the first, and pip-sync makes the environment match it. The
separation keeps the human declaration readable and the machine lock exact.
On many Linux distributions the system interpreter is marked externally
managed. A pip install then fails with a message about PEP 668, and the fix is
a virtual environment rather than a flag that disables the check.
Command line tools that are not project dependencies belong in pipx or uvx,
which give each tool its own isolated environment.
4. pyproject.toml: The Project Contract
pyproject.toml is the standard place to declare a build system and project
metadata. PEP 518 introduced the file so a build front end could install the
backend's requirements before asking it to build. PEP 621 later standardized
the [project] table, and subsequent PEPs added license, dependency group and
lock file conventions. The full file
specification is maintained by the PyPA.
[build-system]
requires = ["hatchling>=1.27"]
build-backend = "hatchling.build"
[project]
name = "analytics-toolkit"
version = "0.3.0"
description = "Reusable analytics helpers"
readme = "README.md"
requires-python = ">=3.11"
license = "MIT"
dependencies = [
"httpx>=0.28,<1",
"polars>=1.0",
]
[project.optional-dependencies]
plot = ["matplotlib>=3.9"]
[dependency-groups]
dev = ["pytest>=9.0", "ruff>=0.17"]
[project.scripts]
analytics = "analytics_toolkit.cli:main"The [build-system] table names the backend and the packages needed to run it.
The front end creates an isolated environment, installs those requirements and
calls the backend's hooks. Common backends include setuptools, hatchling,
flit_core, poetry-core and uv_build. The backend choice is mostly about
features and conventions; the resulting wheel follows the same standard.
The [project] table holds the name, version, description, readme, Python
requirement, license and runtime dependencies. license = "MIT" is the SPDX
string form standardized by PEP 639, which replaced the older table with a
text or file key. The name and version become the
distribution identity on the index, so both matter before the first publish.
[project.optional-dependencies] defines extras that a user can request with
pip install "analytics-toolkit[plot]". [dependency-groups] defines
development groups that are not part of the published package metadata, which
is the right place for pytest, ruff and documentation tools.
The older extras mechanism still works, but a development tool does not belong
in a wheel's dependency metadata.
[project.scripts] creates a console command. The value is a module path, a
colon and the function to call. After installation, analytics runs that
function with the environment's interpreter, which means the command and the
package cannot drift apart.
python -m pip install -e ".[plot]"
python -m pip install -e ".[plot,dev]"The editable install points the environment at the source tree, so a code
change takes effect without reinstalling. It still reads the dependencies and
extras from pyproject.toml, which makes it the normal command for local
development. A regular pip install . builds and installs a wheel, which is the
right check before publishing.
requires-python is a promise, not a suggestion. The resolver uses it to
choose compatible dependency versions, and pip refuses an install when the
running interpreter is outside the range. Setting it too low lets the
resolver install packages that your code cannot run, and setting it too high
rejects users who would be fine.
A library usually declares wide ranges and lets the application lock exact versions. An application usually commits its lock and pins its own runtime image. Mixing the two habits produces either a library that cannot coexist with its siblings or an application that changes under its feet. The next sections show how the two modern tools apply that rule.
5. uv: One Tool for Python, Environments and Locks
uv is a package and project manager written in Rust. It can install Python
interpreters, create environments, resolve dependencies, write a universal lock
file and run commands inside the project. The current release on PyPI is
0.13.7, and the commands below are stable across the 0.13 line.
uv python install 3.15
uv init --lib analytics-toolkit --vcs none
cd analytics-toolkit
uv add "httpx>=0.28,<1"
uv add --dev pytest ruff
uv sync
uv run python -m pytestuv python install downloads a managed interpreter, so the project does not
depend on the system Python. uv init --lib creates a source layout with
pyproject.toml, a src package, a README, a .python-version file and no
version control when --vcs none is set. uv add updates the declaration,
resolves the graph, writes uv.lock and syncs the environment in one step. The
--dev flag places the tools in the dev dependency group.
The video below is a practical introduction to the same workflow. It demonstrates project creation, dependency installation and command execution with uv, which is useful before the deeper reference material.
Description
A video tutorial that introduces uv as a Python package and project manager. It shows how to create a project, add dependencies, run code inside the managed environment and use uv in place of several older tools.
The project layout is small and predictable.
analytics-toolkit/
βββ .python-version
βββ .venv/
βββ pyproject.toml
βββ README.md
βββ src/
β βββ analytics_toolkit/
β βββ __init__.py
βββ uv.lockuv.lock is a universal lock file. It records the resolved versions for every
supported platform and Python marker, not just the machine that generated it.
That is why one lock can serve Linux CI, a macOS laptop and a Windows
developer, provided the declarations agree. The file is TOML, human readable
and managed by uv, so it should be committed and never edited by hand.
Description
A sequence diagram of uv add httpx. The developer runs the command, uv reads pyproject.toml, asks the package index for candidate versions, resolves the graph, writes the new constraint and uv.lock, syncs the project environment, and reports the installed version.
uv sync installs exactly what the lock records, and uv run performs a sync
first when needed. In continuous integration the --locked flag turns a stale
lock into an error instead of a silent update. The --frozen flag skips the
freshness check and uses the lock as written, which is useful when the
declaration is intentionally newer than the lock during a migration.
uv lock --check
uv sync --locked
uv run --locked python -m pytest
uv sync --no-dev
uv add --group docs mkdocs
uv sync --all-groupsuvx runs a command from a package without adding it to the project. It is the
one-off tool runner in the uv family and the right way to run a formatter or a
linter in a repository that does not depend on it.
uvx ruff check .
uvx --from httpie http GET https://example.comFor an existing pip workflow, uv pip accepts the same requirement syntax and
installs into the active environment without creating a project. That makes it
a drop-in speed improvement for a legacy script, and a migration path rather
than a forced rewrite.
Use uv run as the entry point for every project command, including tests,
linters and the application itself. It guarantees that the command sees the
project environment and that the lock is current, which removes the "works in
my shell" class of failures from CI.
6. Poetry: Groups, Environments and Publishing
Poetry is a project manager with a long publishing history. It declares
dependencies in pyproject.toml, resolves them into poetry.lock, creates a
virtual environment and runs commands through poetry run. The current release
is 2.5.1, and version 2.x reads the standard [project] table while still
supporting the legacy [tool.poetry] sections.
poetry new analytics-toolkit
cd analytics-toolkit
poetry add "httpx>=0.28,<1"
poetry add --group dev pytest ruff
poetry install
poetry run pytestpoetry new creates a package layout with a README, a test directory and a
pyproject.toml. poetry add updates the declaration, resolves the graph and
writes poetry.lock. poetry install installs the locked versions and the
project itself in editable mode, which is the normal development command.
poetry run executes a command inside the project environment without
requiring activation.
Dependency groups are part of the Poetry workflow. The main group is for
runtime dependencies, and named groups hold test, documentation and lint tools.
Groups declared under [dependency-groups] follow PEP 735, while groups under
tool.poetry.group.<name> are Poetry specific. Both are selected by the same
install flags.
poetry install --without dev
poetry install --only main
poetry install --all-groups
poetry add --group docs mkdocsPoetry manages the environment for you, usually in a cache directory outside
the project. poetry env info --path prints its location, and
poetry config virtualenvs.in-project true moves it to .venv inside the
project. The latter is convenient for editors and container builds because the
path is predictable. Poetry 2.x also provides poetry env activate, which
prints the shell command that activates the environment.
poetry env info --path
poetry config virtualenvs.in-project true
eval $(poetry env activate)
poetry check --lockpoetry check validates the declaration and its consistency with
poetry.lock, and the --lock flag adds a check that the lock exists. It
catches the common case where someone edited pyproject.toml and forgot to
relock, which is the CI equivalent of uv lock --check.
Poetry 2.x can read the standard [project] table, but many existing projects
still use [tool.poetry.dependencies]. Both forms work, and a migration can
be incremental. Pick one form per project so reviewers do not have to compare
two sources of truth.
The choice between uv and Poetry is less about features than about workflow. uv treats the interpreter, the environment and the lock as one fast pipeline. Poetry has a mature publishing workflow, a long established group model and a large body of documentation. Both write a lock file, both support dependency groups, and both can build and publish a package. The next section explains what the locks actually contain.
7. Lock Files and Reproducible Installs
A lock file answers one question: which exact artifacts were resolved for this project? It records versions, hashes and often the source index. It is generated from the declaration, committed to version control and installed without re-resolving on the target machine. A declaration without a lock is a range of possibilities, not a reproducible build.
uv.lock is universal and specific to uv. poetry.lock is specific to Poetry.
requirements.txt generated by pip-compile is specific to pip, although it is
the most portable of the three because any pip can read it. None of these files
should be edited by hand. When the declaration changes, regenerate the lock and
review the diff like any other code change.
Description
A state diagram of a dependency lock. The project starts with declared ranges, the resolver produces a lock, the environment is synced from it, and a later edit to the declaration marks the lock as stale. A developer relocks and syncs, while continuous integration runs a locked install and refuses to change the graph.
The CI commands differ only in spelling. With uv, uv sync --locked fails when
the lock is out of date and then installs the locked graph. With Poetry,
poetry install installs the lock by default, and poetry check --lock catches
a mismatch first. With pip-tools, pip-sync makes the environment match the
compiled file exactly, removing packages that are not listed.
# uv
uv sync --locked
uv run --locked python -m pytest
# Poetry
poetry check --lock
poetry install --without dev
# pip-tools
pip-compile pyproject.toml -o requirements.txt
pip-sync requirements.txtFor a hash checked pip install, generate the hashes at compile time and keep the resulting file under version control. The install command then verifies every artifact.
pip-compile --generate-hashes pyproject.toml -o requirements.txt
python -m pip install --require-hashes -r requirements.txtPEP 751 standardized a tool agnostic lock format called pylock.toml. The
format records packages, versions, wheels, hashes and markers, and it is
designed to be read by any compliant installer. pip 26.1 can read a
pylock.toml through -r on an experimental basis, and uv can export its lock
to the format with uv export --format pylock.toml.
uv export --format pylock.toml -o pylock.toml
python -m pip install -r pylock.tomlThe practical advice for 2026 is conservative. Keep the native lock that the
project manager understands, because it carries the richest information and
the best error messages. Export pylock.toml when another team or a deployment
platform needs a standard artifact, and treat pip's support as experimental
until the command stops being marked that way.
A lock file is only as trustworthy as its source. A lock generated from a private index, a Git branch or a local path can point at code that another machine cannot fetch. Record the index in the project configuration, prefer immutable versions and hashes, and review every lock diff that changes a package source.
8. Building and Publishing a Distribution
A source distribution contains source code and build metadata. A wheel contains a built package with a filename that declares the Python version, the ABI and the platform. Pure Python projects usually produce a universal wheel that runs anywhere. Projects with compiled extensions produce one wheel per platform and Python version, which is why the build matrix grows.
The build front end reads [build-system], creates an isolated environment,
installs the backend requirements and calls the backend hooks. python -m build produces both an sdist and a wheel in dist/, and uv build and
poetry build do the same job with their own conventions.
python -m build
python -m twine check dist/*
python -m twine upload --repository testpypi dist/*twine check validates the metadata and the long description before an upload.
TestPyPI is a separate index for rehearsal, and its dependency set is not
complete, so a test install may need --extra-index-url to reach the real
index. A version that has been uploaded cannot be replaced, even after it is
deleted from the index, so bump the version for every attempt.
python -m pip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
analytics-toolkitThe modern alternatives wrap the same steps. uv build writes to dist/, and
uv publish uploads with a token or trusted publishing. poetry build and
poetry publish do the same inside the Poetry workflow. Choose the one that
already owns the project environment, and keep the release command in a script
so it is reviewable.
uv build
uv publish
poetry build
poetry publishTrusted publishing removes the long lived API token. The CI provider proves its
identity to PyPI with an OpenID Connect token, and PyPI issues a short lived
credential for the upload. The repository needs a trusted publisher entry, the
job needs id-token: write, and the upload command must run in that job. The
result is a release that cannot leak a reusable token from a log or a
repository secret.
Description
A timeline of packaging standards and tools. PEP 518 introduced pyproject.toml in 2016, PEP 621 standardized project metadata in 2020, PEP 735 standardized dependency groups in 2024, PEP 751 standardized pylock.toml in 2025, Poetry 2.0 shipped in 2025, and pip 26.1 added experimental pylock installs in 2026.
A release is more than an upload. The version must match the tag, the metadata
must match the README, and the wheel must be the artifact that was tested. A
short release checklist prevents most of the failures: run the tests from the
built wheel in a clean environment, check the version in pyproject.toml, build
once, inspect dist/, upload to TestPyPI, install from TestPyPI, then publish.
Never upload a package that embeds a token, a private key or an internal URL. Wheel contents are public and permanent. Build in a clean environment, inspect the archive, and treat the release job as a security boundary rather than a convenience script.
9. Real-World Case Studies
The two cases below show the same principles in a continuous integration job and in a migration from an older requirements workflow.
Case 1: A locked CI install with uv
A repository has pyproject.toml and uv.lock, and the test job must fail when
the lock is stale. The workflow installs uv, uses the committed Python version
and runs the tests through the project environment.
name: test
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v10
with:
version: "0.13.7"
enable-cache: true
- run: uv python install
- run: uv sync --locked
- run: uv run --locked python -m pytestuv python install reads .python-version, so the job uses the same
interpreter as the developers. uv sync --locked fails if uv.lock does not
match pyproject.toml, which prevents a CI run from silently resolving a
different graph. uv run --locked repeats the check and then executes pytest
inside the environment. The cache shortens the install without changing the
resolved versions.
The same job without uv uses actions/setup-python, pip install -r requirements.txt and a pinned requirements file. That works, but the versions
are only as exact as the file, and a lock generated on another platform may not
match. The uv job keeps the universal lock and the interpreter version in the
repository, so the only moving part is the action itself.
Case 2: Migrating a requirements project to uv
A legacy service has requirements.txt with direct and transitive packages
mixed together. The migration starts by creating a project declaration and
importing the existing constraints, then verifies that the resolved graph still
passes the tests.
uv init --bare
uv add -r requirements.txt
uv lock
uv sync
uv run python -m pytestuv init --bare creates only pyproject.toml, which avoids restructuring the
source tree during the first step. uv add -r reads the requirements file and
adds its entries as project dependencies. uv lock resolves the graph and
writes uv.lock, and uv sync installs it. The old file stays in the
repository until the team confirms that the new graph passes the full suite.
uv export --format requirements.txt -o requirements.lock.txt
python -m pip install -r requirements.lock.txtThe export gives deployment platforms a plain requirements file while the
project keeps uv.lock as the source of truth. The generated file is a build
artifact, not a declaration, so it should be regenerated on every release and
never edited by hand. A team that needs hashes can add them during the export
or continue to use pip-compile --generate-hashes for the deployment file.
A migration is complete when the new lock can recreate a passing environment from a clean checkout. Deleting the old requirements file before that check is not a migration, it is a bet. Keep both paths working until the new one has run in CI and in production for at least one release.
10. Choosing a Tool, Pitfalls and a Review Checklist
The tools overlap, so the decision should be based on the workflow rather than on a feature checklist. The table below is a starting point, not a rule.
Situation | Recommended default | Reason |
|---|---|---|
New application or library | uv | One tool covers Python, environment, lock and run |
Team already standardized on Poetry | Poetry | Existing groups, CI and publishing stay unchanged |
Minimal script with no dependencies | venv and pip | No extra tool to install or learn |
Legacy deployment needs requirements | pip-tools or uv export | The platform accepts a plain requirements file |
Tools outside any project | uvx or pipx | Each tool gets an isolated environment |
The quadrant below places the main options on two axes: how much of the workflow the tool covers, and how established its adoption is.
Description
A quadrant chart comparing Python packaging tools. The horizontal axis runs from a minimal workflow to a complete workflow, and the vertical axis runs from newer adoption to established adoption. pip with venv sits top left, pip-tools sits near the middle, Poetry sits top right, and uv sits bottom right.
The pitfalls are mostly about mixing layers. A declaration is not a lock, a lock is not a declaration, and an environment is neither. Keep the three separate and the failure modes become visible.
- Committing
.venv/,dist/,build/or*.egg-info/instead of the lock and the declaration. - Installing project dependencies into the system interpreter, especially with
sudo. - Running
pip freezein a polluted environment and calling the result a declaration. - Editing a lock file by hand to force a version instead of changing the constraint and relocking.
- Using an unbounded
>=for an application dependency and expecting the next install to behave like the last one. - Forgetting
requires-pythonor setting it wider than the code supports. - Publishing a version that was already uploaded and expecting PyPI to replace it.
- Storing a PyPI API token in a repository secret when trusted publishing is available.
A short review before merging packaging changes: does the declaration state direct dependencies and a Python range, does the lock cover the full graph, does CI install with the lock instead of re-resolving, and does the release job build the artifact it tested?
2026 Updates (as of November 2026)
- Python 3.15.2 is the current stable release, and the packaging examples use 3.11 or newer so they remain compatible with the supported branches.
- uv 0.13.7 is the current release on PyPI, and its project workflow covers Python installation, environments, universal locks and publishing.
- Poetry 2.5.1 is the current release. Poetry 2.x reads the standard
[project]table, supports PEP 735 dependency groups and keepspoetry.lockas its native lock. - PEP 751 standardized
pylock.tomlin 2025. pip 26.1 added experimental support for installing from one, and uv can export its lock to the format. - PEP 639 moved the license field to an SPDX string such as
license = "MIT". The older table form is still seen in legacy projects. - PEP 735 dependency groups are now the standard place for development tools, and both uv and Poetry understand them.
- PEP 668 remains the reason a system Python refuses a direct install. The virtual environment is the supported answer, not a flag that disables the protection.
The direction is clear even where the details are still moving. Declarations
are converging on pyproject.toml, locks are converging on a small number of
well understood formats, and publishing is converging on short lived identity
rather than stored tokens. A project that keeps those three layers separate can
change tools without changing its release process.
π Did you know?
venvhas been in the standard library since Python 3.3, while the oldervirtualenvpackage still exists for features and interpreter versions thatvenvdoes not cover.- The name
pipis commonly expanded as "Pip Installs Packages", a recursive acronym in the style of other package managers. - uv is written in Rust and can download and manage Python interpreters itself, so a machine does not need a system Python to start a project.
- PyPI treats a version as permanent. Deleting a release does not free the version number for a different artifact.
Practice Questions
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A developer creates a virtual environment for a project and installs packages into it. What does the environment isolate?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A script is run with .venv/bin/python, but the developer forgot to activate the environment. Which value confirms that the project environment is in use?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
What is the difference between pip freeze and pip list?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A dependency is declared as pandas~=2.3.0. Which versions satisfy that specifier?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
Which pyproject.toml table tells a build front end which backend to install and call?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A CI job runs uv sync --locked and fails because pyproject.toml changed after the lock was generated. What is the correct response?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A Poetry project has a dev group with pytest and ruff. Which command installs the runtime dependencies without that group?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
Why should a lock file be committed to version control while .venv is ignored?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
What problem does the PEP 751 pylock.toml format solve?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A small script needs to read the [project] table from pyproject.toml without adding a dependency. Which standard library module parses the file?
Key Takeaways
- A virtual environment isolates packages, a declaration records intent, and a lock records the exact resolved graph.
venvis sufficient to create an environment; activation is optional and the direct interpreter path is more reliable in scripts.pipinstalls packages, while a resolver and a lock decide which versions the whole graph receives.pyproject.tomldeclares the build system, metadata, dependencies, extras, dependency groups and console scripts.- uv combines Python installation, environment creation, resolution, locking and command execution in one fast workflow.
- Poetry provides the same project model with mature dependency groups and a
publishing workflow built around
poetry.lock. - A lock file is committed and installed without re-resolving, and CI should fail when the lock is stale.
- A release builds once, tests the artifact, rehearses on TestPyPI and publishes with trusted identity rather than a stored token.
References
Every factual claim links to its source, with the access date printed so the source can be rechecked.