Virtual Environments and Packaging
|
This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Two projects on the same machine often need different, conflicting versions of the same third-party package. A virtual environment gives each project its own private set of installed packages, so installing one project’s dependencies never breaks another’s.
Why Virtual Environments
Without isolation, every pip install writes into the one global Python installation shared by every script
and project on the machine. Upgrading a package for project A can silently break project B, and there is no
record of which packages a given project actually needs. A virtual environment is a self-contained directory
holding its own Python interpreter (or a link to one) and its own site-packages — packages installed while it
is active are invisible to any other environment. The standard library ships this as the venv module,
covered in the tutorial’s Virtual Environments and Packages
chapter and specified in full under venv.
Creating and Activating an Environment
Create one with python -m venv, naming the directory that will hold it — .venv is the common convention:
python -m venv .venv
Activation adjusts the current shell so python and pip resolve to the environment’s own copies; the command
differs per OS and shell:
# macOS / Linux (bash or zsh)
source .venv/bin/activate
# Windows (Command Prompt)
.venv\Scripts\activate.bat
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
Once active, the shell prompt is usually prefixed with the environment’s name, and python -m pip list shows
only what has been installed inside it:
(.venv) $ which python
/path/to/project/.venv/bin/python
Deactivate with a single command that works the same on every platform:
deactivate
Deleting the environment is just deleting its directory — nothing outside .venv/ is ever touched:
rm -rf .venv
Installing Packages with pip
pip is the package installer bundled with Python; it downloads a package and its dependencies and installs
them into whichever environment is currently active. Its full command reference lives at
pip documentation.
pip install requests
pip install requests==2.31.0 # pin an exact version
pip install "requests>=2.28,<3" # constrain a range
Uninstalling removes a package the same way:
pip uninstall requests
Freezing and requirements.txt
pip freeze prints every installed package with the exact version currently resolved, in a format pip can
read back:
pip freeze > requirements.txt
certifi==2024.7.4
charset-normalizer==3.3.2
idna==3.7
requests==2.32.3
urllib3==2.2.2
Committing requirements.txt to version control lets anyone — a teammate, or a CI job — recreate the exact
same set of installed packages in a fresh environment:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
PyPI and pyproject.toml
PyPI (the Python Package Index, https://pypi.org) is the public repository pip install <name> downloads
from by default — searching it by name or keyword is how most third-party packages are discovered before they
are added to a project. The full packaging ecosystem built around it — how a project declares its own metadata,
dependencies, and build process — is documented at the Python Packaging User
Guide.
A modern project declares that metadata in a single file, pyproject.toml, at its root. Unlike
requirements.txt (a flat list of installed versions), pyproject.toml describes the project itself — its
name, dependencies, and the tool used to build a distributable package from it:
[project]
name = "my-project"
version = "0.1.0"
description = "A small example project"
dependencies = [
"requests>=2.28",
]
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
The [build-system] table only names which build backend to use (setuptools above); the backend itself is a
separate tool responsible for turning the project into an installable distribution, a topic the Packaging User
Guide covers on its own. The rest of this section covers building and publishing a release with it.
Building Distributions
python -m build (the build package, pip install build) reads pyproject.toml and produces two
distributable files under dist/: a sdist (.tar.gz, the source distribution) and a wheel (.whl, a
prebuilt distribution pip installs without running the build backend again). The [build-system] table above
only names which backend does that work — setuptools is this page’s example, but hatchling and
poetry-core are interchangeable alternatives that read the same [project] metadata:
python -m build # writes dist/my_project-0.1.0.tar.gz and dist/my_project-0.1.0-py3-none-any.whl
Creating PyPI and TestPyPI Accounts
Publishing needs an account at pypi.org, and PyPI requires two-factor authentication to be enabled on that account before it will accept a publish. A separate instance, test.pypi.org, exists specifically for dry-run publishes — it has its own independent account and package namespace, so a broken upload or a name collision there never touches the real index.
Generating an API Token
Account Settings → API tokens creates a scoped token; PyPI recommends scoping it to a single project rather
than the whole account, which is only possible after that project’s first manual upload has created it on the
index. The token is used with twine as a username/password pair, where the username is always the literal
string token:
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-AgEIcHlwaS5vcmc... # the generated token, including its pypi- prefix
Publishing with twine
twine (pip install twine) uploads the files python -m build produced. A dry run against TestPyPI first
catches metadata or packaging problems without touching the real index:
twine upload --repository testpypi dist/*
twine upload dist/* # the real release, once the TestPyPI upload looks right
PyPI Trusted Publishing (OIDC)
The recommended approach for a CI-driven release is PyPI’s Trusted Publishing, which needs no stored API token at all. A Trusted Publisher is configured once on pypi.org (under the project’s Publishing settings), naming the exact GitHub repository, workflow filename, and (optionally) environment allowed to publish; PyPI then accepts a GitHub Actions OIDC token asserting the workflow run matches, in place of any credential:
name: build
on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write # required for PyPI Trusted Publishing
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: 'pip'
- run: pip install build
- run: python -m build
- name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish@release/v1
See Also
-
Modules and Packages — how
importandsys.pathfind the packages installed into an environment. -
Standard Library Tour — modules that ship with Python and need no
pip installat all.