Packaging, Environments and Dependency Management
Intermediate⏱80 minutesπŸ“–22 min readLesson 8 of 10

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

Virtual environment
A directory that contains a Python interpreter and its own site-packages, isolated from the system interpreter and from other projects.
Dependency
A package that a project needs in order to run or to be tested, declared with a version constraint.
Direct dependency
A package that the project declares itself, as opposed to one that is pulled in by another package.
Transitive dependency
A package required by one of the project's dependencies, resolved automatically and recorded in the lock file.
Dependency resolver
The algorithm that chooses a set of package versions that satisfies every constraint at once.
Lock file
A generated file that records the exact versions and hashes the resolver selected for the whole dependency graph.
Wheel
A built binary distribution with a .whl suffix that pip installs without running a build step.
Source distribution
A distribution containing source code and build metadata, usually a .tar.gz file, that the installer must build.
Editable install
An install that points the environment at the project source tree so code changes take effect without reinstalling.
Externally managed environment
A system interpreter that a distribution marks as off limits for pip installs, defined by PEP 668.
Dependency group
A named set of development-only dependencies in pyproject.toml, standardized by PEP 735.
Trusted publishing
A PyPI upload flow that exchanges a short lived OpenID Connect token for permission instead of storing a long lived API token.

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.

100%
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.

ℹ️Note

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.

bash
python3 -m venv .venv source .venv/bin/activate python -m pip --version deactivate

On Windows PowerShell the activation script is different, and the rest of the workflow is the same.

powershell
python -m venv .venv .venv\Scripts\Activate.ps1 python -m pip --version deactivate

Activation 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.

bash
.venv/bin/python -m pip install "httpx>=0.28,<1" .venv/bin/python -m pytest

Inside 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.

python
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.

⚠️Warning

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.

bash
python -m pip install "requests>=2.32,<3" python -m pip install -r requirements.txt python -m pip list --outdated python -m pip check

A 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.

plaintext
# 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.

bash
python -m pip freeze > requirements.txt python -m pip install --require-hashes -r requirements-lock.txt

A 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.

πŸ’‘Tip

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.

toml
[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.

bash
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.

πŸ“ŒImportant

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.

bash
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 pytest

uv 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.

Shaw TalebiWatch on YouTube
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.

plaintext
analytics-toolkit/ β”œβ”€β”€ .python-version β”œβ”€β”€ .venv/ β”œβ”€β”€ pyproject.toml β”œβ”€β”€ README.md β”œβ”€β”€ src/ β”‚ └── analytics_toolkit/ β”‚ └── __init__.py └── uv.lock

uv.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.

100%
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.

bash
uv lock --check uv sync --locked uv run --locked python -m pytest uv sync --no-dev uv add --group docs mkdocs uv sync --all-groups

uvx 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.

bash
uvx ruff check . uvx --from httpie http GET https://example.com

For 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.

πŸ’‘Tip

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.

bash
poetry new analytics-toolkit cd analytics-toolkit poetry add "httpx>=0.28,<1" poetry add --group dev pytest ruff poetry install poetry run pytest

poetry 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.

bash
poetry install --without dev poetry install --only main poetry install --all-groups poetry add --group docs mkdocs

Poetry 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.

bash
poetry env info --path poetry config virtualenvs.in-project true eval $(poetry env activate) poetry check --lock

poetry 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.

ℹ️Note

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.

100%
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.

bash
# 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.txt

For 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.

bash
pip-compile --generate-hashes pyproject.toml -o requirements.txt python -m pip install --require-hashes -r requirements.txt

PEP 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.

bash
uv export --format pylock.toml -o pylock.toml python -m pip install -r pylock.toml

The 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.

⚠️Warning

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.

bash
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.

bash
python -m pip install \ --index-url https://test.pypi.org/simple/ \ --extra-index-url https://pypi.org/simple/ \ analytics-toolkit

The 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.

bash
uv build uv publish poetry build poetry publish

Trusted 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.

100%
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.

🚨Danger

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.

yaml
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 pytest

uv 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.

bash
uv init --bare uv add -r requirements.txt uv lock uv sync uv run python -m pytest

uv 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.

bash
uv export --format requirements.txt -o requirements.lock.txt python -m pip install -r requirements.lock.txt

The 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.

πŸ“ŒImportant

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.

100%
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 freeze in 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-python or 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.
πŸ“ŒImportant

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 keeps poetry.lock as its native lock.
  • PEP 751 standardized pylock.toml in 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?

  • venv has been in the standard library since Python 3.3, while the older virtualenv package still exists for features and interpreter versions that venv does not cover.
  • The name pip is 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

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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?

Practice Question
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?

βœ…Success

Key Takeaways

  • A virtual environment isolates packages, a declaration records intent, and a lock records the exact resolved graph.
  • venv is sufficient to create an environment; activation is optional and the direct interpreter path is more reliable in scripts.
  • pip installs packages, while a resolver and a lock decide which versions the whole graph receives.
  • pyproject.toml declares 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.

  1. Python Software Foundation Β· Accessed 2026-11-07

  2. [2]pip installDocumentation

    Python Packaging Authority Β· Accessed 2026-11-07

  3. [3]Requirements file formatDocumentation

    Python Packaging Authority Β· Accessed 2026-11-07

  4. Python Software Foundation Β· Accessed 2026-11-07

  5. Python Software Foundation Β· Accessed 2026-11-07

  6. Python Software Foundation Β· Brett Cannon, Nathaniel J. Smith, Donald Stufft Β· Accessed 2026-11-07

  7. Python Software Foundation Β· Brett Cannon, Dustin Ingram, Paul Ganssle, Pradyun Gedam, Thomas Kluyver Β· Accessed 2026-11-07

  8. Python Packaging Authority Β· Accessed 2026-11-07

  9. Python Software Foundation Β· Accessed 2026-11-07

  10. Python Software Foundation Β· Accessed 2026-11-07

  11. [11]uv 0.13.7 on PyPIRepository

    Python Package Index Β· Accessed 2026-11-07

  12. Astral Β· Accessed 2026-11-07

  13. YouTube Β· Shaw Talebi Β· Accessed 2026-11-07

  14. Astral Β· Accessed 2026-11-07

  15. [15]Poetry 2.5.1 on PyPIRepository

    Python Package Index Β· Accessed 2026-11-07

  16. Poetry Β· Accessed 2026-11-07

  17. Poetry Β· Accessed 2026-11-07

  18. Poetry Β· Accessed 2026-11-07

  19. Python Software Foundation Β· Accessed 2026-11-07

  20. Python Packaging Authority Β· Accessed 2026-11-07

  21. Python Package Index Β· Accessed 2026-11-07

  22. Astral Β· Accessed 2026-11-07

  23. [23]Python 3.15 documentationDocumentation

    Python Software Foundation Β· Accessed 2026-11-07

  24. [24]pip release notesDocumentation

    Python Packaging Authority Β· Accessed 2026-11-07

Progress80%