Development¶
Getting started¶
virtualenv is a volunteer maintained open source project and we welcome contributions of all forms. The sections
below will help you get started with development, testing, and documentation. We’re pleased that you are interested in
working on virtualenv. This document is meant to get you setup to work on virtualenv and to act as a guide and reference
to the development setup. If you face any issues during this process, please open an issue about it on the issue
tracker.
Setup¶
virtualenv is a command line application written in Python. To work on it, you’ll need:
- Source code: available on GitHub. You can use
gitto clone the repository:
git clone https://github.com/pypa/virtualenv cd virtualenv
- Source code: available on GitHub. You can use
Python interpreter: We recommend using
CPython. You can use this guide to set it up.tox: to automatically get the projects development dependencies and run the test suite. We recommend installing it using pipx.
Running from source tree¶
The easiest way to do this is to generate the development tox environment, and then invoke virtualenv from under the
.tox/dev folder
tox -e dev
.tox/dev/bin/virtualenv # on Linux
.tox/dev/Scripts/virtualenv # on Windows
Running tests¶
virtualenv’s tests are written using the pytest test framework. tox is used to automate the setup and execution of virtualenv’s tests.
To run tests locally execute:
tox -e py
This runs the test suite on Python 3.15. Select another supported interpreter with its environment name, such as
3.9, 3.14 or pypy3.11.
tox has been configured to forward any additional arguments it is given to pytest. This enables the use of
pytest’s rich CLI. As an example, you
can select tests using the various ways that pytest provides:
# Using markers
tox -e py -- -m "not slow"
# Using keywords
tox -e py -- -k "test_extra"
Some tests require additional dependencies to be run, such is the various shell activators (bash, fish,
powershell, etc). These tests will automatically be skipped if these are not present, note however that in CI all
tests are run; so even if all tests succeed locally for you, they may still fail in the CI.
The tests under tests/tasks cover the build and CI tooling, hatch_build.py and the scripts in tasks/. Their
outcome does not depend on the interpreter, so the interpreter environments skip them and one environment runs them on
CPython 3.15:
tox -e tasks
Running linters¶
virtualenv uses pre-commit for managing linting of the codebase. pre-commit performs various checks on all
files in virtualenv and uses tools that help follow a consistent code style within the codebase. To use linters locally,
run:
tox -e fix
Note
Avoid using # noqa comments to suppress linter warnings - wherever possible, warnings should be fixed instead.
# noqa comments are reserved for rare cases where the recommended style causes severe readability problems.
Type checking¶
virtualenv ships a PEP 561 py.typed marker and has comprehensive type annotations across the entire codebase.
This means downstream consumers and type checkers automatically recognize virtualenv as an inline-typed package.
All new code must include complete type annotations for function parameters and return types. To verify annotations locally, run:
tox -e type
This uses ty (Astral’s Rust-based type checker) to validate annotations against Python 3.15. A second environment checks compatibility with the minimum supported version:
tox -e type-3.9
Both environments validate that annotations are consistent and correct.
Annotation guidelines¶
Use
from __future__ import annotationsat the top of every module (enforced by ruff’srequired-importssetting).Place imports that are only needed for type checking inside an
if TYPE_CHECKING:block to avoid runtime overhead.Ruff’s
ANNrules are enabled.ANN401(typing.Any) is suppressed on a case-by-case basis with inline# noqa: ANN401comments whereAnyis genuinely required (e.g. serialization, dynamic dispatch).Prefer concrete types over
Any. UseUnion/|for nullable or multi-type parameters.When a type error is genuinely unfixable (e.g. third-party library limitations), suppress it with an inline
# ty: ignore[rule-name]comment and a brief justification.
Building documentation¶
virtualenv’s documentation is built using Sphinx. The documentation is written in reStructuredText. To build it locally, run:
tox -e docs
The built documentation can be found in the .tox/docs_out folder and may be viewed by opening index.html within
that folder.
Logo and branding¶
Two logo files live in docs/_static and serve different roles, so both are kept on purpose:
virtualenv.pngis the official logo — the snakes wrapped around a terminal, with the wordmark. It is the site logo (light_logo/dark_logoindocs/conf.py) and the one to use wherever a virtualenv logo is needed.virtualenv.svgis a simplified mark used only as the browser favicon (html_favicon), where the detailed PNG would be illegible at 16×16. It is not a vector copy of the PNG.
Release¶
virtualenv’s release schedule is tied to pip and setuptools. We bundle the latest version of these libraries so
each time there’s a new version of any of these, there will be a new virtualenv release shortly afterwards (we usually
wait just a few days to avoid pulling in any broken releases).
Performing a release¶
A full release publishes to PyPI, creates a GitHub Release with the zipapp and SBOMs attached, and updates get-virtualenv so that https://bootstrap.pypa.io/virtualenv.pyz serves the new version.
Version bumping¶
The --version argument to tox r -e release controls the version. It defaults to auto, which inspects the
docs/changelog directory: if any *.feature.rst or *.removal.rst fragments exist, the minor version is
bumped, otherwise the patch version is bumped. You can also pass major, minor, or patch explicitly.
Both methods produce identical results: a release commit and tag on main. Pushing the tag triggers the Release
workflow which builds the sdist, wheel, and
zipapp, publishes to PyPI via trusted publisher, creates a GitHub Release with the zipapp and SBOMs attached, and updates get-virtualenv. A failed publish needs the recovery procedure below.
Via GitHub Actions (recommended)
Go to the Pre-release workflow on GitHub.
Click Run workflow and select the bump type (
auto,major,minor, orpatch).
Locally
tox r -e release
Pass --version <bump> to override the default auto behavior (e.g. --version minor).
Recovering a partial publication¶
A failure after the PyPI upload can leave a published package without its GitHub release or bootstrap update. Keep the
release commit and tag: users may have installed that version, and PyPI does not allow filename reuse. Resetting the tip of main can also remove an unrelated commit that a
maintainer merged during publication.
Before retrying, inspect the failed job, the files on PyPI, the GitHub release assets and
get-virtualenv/public/version.txt. Compare their hashes with the artifacts from the failed workflow run. Preserve
those artifacts and logs. A timeout does not prove that an upload failed.
Complete missing publication steps with the original verified artifacts. Do not rerun the full publish job after PyPI
accepted its upload; the duplicate upload can fail before reaching the remaining destinations. If the artifacts need a
change, publish a new version and consider yanking the broken release. Use a normal reviewed commit for any bootstrap
correction, without force-pushing either repository. Follow .github/INCIDENT_RESPONSE.md if there is evidence of
tampering or compromised credentials.
The release workflow verifies the published wheel, sdist and zipapp after publication. It checks the PyPI attestations
and compares the distributions with the build artifacts, verifies the zipapp’s release workflow, tag and commit, checks
that the virtualenv.cdx.json and virtualenv.spdx.json release assets match the wheel and the attested SBOMs,
then creates an environment from each distribution. This job has no publishing credentials. A verification failure does
not undo publication: inspect the mismatch or creation failure before retrying only the verification job. Do not rerun
publication to repair a verification failure.
Contributing¶
Submitting pull requests¶
Submit pull requests against the main branch, providing a good description of what you’re doing and why. You must
have legal permission to distribute any code you contribute to virtualenv and it must be available under the MIT
License. Provide tests that cover your changes and run the tests locally first. virtualenv supports multiple Python versions and operating systems. Any pull request must consider and work on
all these platforms.
Pull Requests should be small to facilitate review. Keep them self-contained, and limited in scope. Studies have shown that review quality falls off as patch size grows. Sometimes this will result in many small PRs to land a single large feature. In particular, pull requests must not be treated as “feature branches”, with ongoing development work happening within the PR. Instead, the feature should be broken up into smaller, independent parts which can be reviewed and merged individually.
Additionally, avoid including “cosmetic” changes to code that is unrelated to your change, as these make reviewing the PR more difficult. Examples include re-flowing text in comments or documentation, or addition or removal of blank lines or whitespace within lines. Such changes can be made separately, as a “formatting cleanup” PR, if needed.
Code review¶
A maintainer reviews each pull request before it merges into main and checks that:
the change does what its description says, and a test covers it;
the change works on each Python version and operating system that Compatibility lists;
a news fragment describes each user-facing change, and the documentation matches the new behavior;
the command line, the Python API and the plugin API stay compatible, or a deprecation or removal fragment announces the break;
a change to activation scripts,
pyvenv.cfg, wheel downloads, app-data or the release workflows names in its description the outside input it handles, and updates the threat model when it hits an update trigger;a new or bumped dependency meets the licensing policy.
The main ruleset enforces the rest. A pull request merges only after four checks pass: ✅ all checks pass, which
gathers the test matrix, the type checks, the docs build, the zipapp and SBOM checks and the CodeQL query tests from
check.yaml; 🔍 dependency review; pre-commit.ci - pr; and the Read the Docs build. The ruleset also blocks a
merge while CodeQL reports an error, or a security alert of high severity or above.
The ruleset asks for one approval from someone other than the last person to push. Repository admins can bypass that rule, and the maintainers merge their own pull requests this way once the required checks pass. The threat model records the risk of a single reviewer.
AI-assisted contributions¶
You may use AI tools (code assistants, chat models, agents) to help write a contribution, under these conditions:
You are the author. Read, understand and test everything you submit; you are responsible for it as if you had typed it yourself, including its licensing.
Keep a human in the loop. Do not open pull requests or issues, or post review comments, that you have not read. Fully automated submissions are closed without review.
Do not paste in code whose license is unknown or incompatible with the MIT License, whatever produced it.
Maintainers may use AI tools to help triage and review. A review comment posted under a maintainer’s name has been read and endorsed by that maintainer.
Dependency policy¶
virtualenv lists its runtime dependencies under [project.dependencies] in pyproject.toml, and its development
and release tools in dependency groups and tox.toml. Adding a runtime dependency is a maintainer decision under the
Licensing policy below.
The project obtains and updates each kind of dependency this way:
Installers fetch runtime dependencies from PyPI within the ranges
pyproject.tomlallows.The daily
upgrade.yamlworkflow runstox r -e upgrade, which downloads newpipandsetuptoolswheels from PyPI intosrc/virtualenv/seed/wheels/embedand records their SHA-256 inBUNDLE_SHA256. The same workflow recompilespylock.zipapp.toml, which pins each distribution the zipapp bundles by version and hash. It leaves out releases younger than seven days and opens a pull request with the result.Dependabot bumps the GitHub Actions, which the repository requires to be pinned to a commit SHA, every week after a seven-day cooldown. pre-commit.ci bumps the pre-commit hooks, which
.pre-commit-config.yamlfreezes to commit SHAs.upgrade.yamlbumps the CI test tools every Monday.
We track dependencies through the CycloneDX SBOM in every wheel and the SBOMs attached to each release, which Release artifacts describes, and through Dependabot alerts, which cover the pip and GitHub Actions manifests.
Software composition analysis findings follow these thresholds:
dependency-review.yamlfails a pull request that adds a dependency with a known advisory of any severity, in runtime or development scope. Themainruleset requires that check.A shipped package whose license falls outside
ALLOWED_LICENSESfails the SBOM check intox r -e readmeandtox r -e zipapp, which the required✅ all checks passcheck runs. We accept no exceptions.An open Dependabot alert blocks the next release. The maintainer who cuts the release fixes it first, or dismisses it with the reason virtualenv does not reach the vulnerable code.
An embedded wheel with no fixed version for a supported Python version stays, and Embedded wheel advisories lists its advisories and the way around them.
Licensing policy¶
virtualenv is distributed under the MIT License, and everything in the repository must be compatible with it:
Contributions are accepted under the MIT License only; you must have the right to license what you submit.
What virtualenv ships must use a permissive license from
ALLOWED_LICENSESintasks/license_policy.py. That covers virtualenv itself, every distribution the zipapp bundles, and the embeddedpipandsetuptoolswheels undersrc/virtualenv/seed/wheels/embed, which keep their own MIT licenses and ship unchanged. The packages those wheels vendor follow the terms of their wheel. The Unlicense entry coversfilelock3.19.1 and older, which virtualenv installs on Python 3.9 and bundles in the zipapp. Copyleft licenses (GPL, LGPL, AGPL, MPL) stay out. Development and release tools carry no such restriction.tasks/validate_sbom.pyenforces the policy on the SBOMs of the built wheel and zipapp, intox r -e readmeandtox r -e zipapp. An SPDX expression passes when eachORoffers an allowed license and everyANDoperand is allowed; aWITHexception leaves the verdict to the license it modifies. A package that declares its license by name alone fails the check until_CLASSIFIER_LICENSESinhatch_build.pymaps the name to an SPDX id..github/workflows/dependency-review.yamlfails a pull request that adds a dependency with a known vulnerability to any manifest, release tooling lock files included.Adding a runtime dependency or bumping an embedded wheel is a maintainer decision; checking the license of the new version is part of that review.
hatch_build.pywrites a CycloneDX SBOM into every wheel at.dist-info/sboms/virtualenv.cdx.json. It reads theBUNDLE_SUPPORT/BUNDLE_SHA256tables that back the embedded wheels above, so a wheel bump updates it with no extra step.tasks/cyclonedx_to_spdx.pyrenders that document as SPDX 2.3 with the standard library alone.tox r -e readmebuilds a wheel and validates its SBOM and the SPDX rendering; the release workflow runstox r -e spdxto writevirtualenv.spdx.jsonfromvirtualenv.cdx.jsonand check it withpyspdxtools. Both environments run on Python 3.15 with a Beartype prerelease that fixes its removed typing API import.tasks/zipapp_sbom.pydescribes a builtvirtualenv.pyzfrom the archive itself: virtualenv, the embedded wheels, and each bundled dependency with the Python versions that load it, down to a SHA-256 per file. It reuses the helpers inhatch_build.py, writesvirtualenv.pyz.cdx.jsonnext to the zipapp and adds the same file to the archive root.tox r -e zipappbuilds the zipapp, runs the script, and hastasks/validate_sbom.pycheck that the SBOM lists every file in the archive with its hash.
Release artifacts describes every file, SBOM and attestation a release publishes, and Verify a virtualenv release shows users how to check them.
Automated testing¶
All pull requests and merges to ‘main’ branch are tested using GitHub Actions
(configured by .github/workflows/check.yaml file at the root of the repository). You can find the status and results
to the CI runs for your PR on GitHub’s Web UI for the pull request. You can also find links to the CI services’ pages
for the specific builds in the form of “Details” links, in case the CI run fails and you wish to view the output.
To trigger CI to run again for a pull request, you can close and open the pull request or submit another change to the pull request. If needed, project maintainers can manually trigger a restart of a job/build.
Code scanning policy¶
Static analysis runs on every pull request and every push to main:
pre-commit.ci runs ruff with every rule selected, the
flake8-banditsecurity rules included, and zizmor on the workflows. Thetypeandtype-3.9tox environments run ty.codeql.yamlruns CodeQL with thesecurity-extendedsuite and the project queries under.github/codeql/querieson the Python code, and the Actions analysis on the workflows. It also runs every Tuesday.
A finding blocks a merge at these thresholds:
Any ruff, zizmor or ty finding fails a required check.
The
mainruleset blocks a merge while CodeQL reports an alert of severityerror, or a security alert of severityhighorcritical.Each other open CodeQL alert blocks the next release. The maintainer who cuts the release fixes it first, or dismisses it as a false positive or as not exploitable.
A CodeQL dismissal carries a comment that says who controls the flagged input and why it cannot cross a trust boundary
that the threat model lists. An inline
suppression names the rule it silences, and Running linters says when a # noqa fits.
NEWS entries¶
The changelog.rst file is managed using towncrier and all non trivial changes must be accompanied by a news
entry. To add an entry to the news file, first you need to have created an issue describing the change you want to make.
A Pull Request itself may function as such, but it is preferred to have a dedicated issue (for example, in case the PR
ends up rejected due to code quality reasons).
Once you have an issue or pull request, you take the number and you create a file inside of the docs/changelog
directory named after that issue number with an extension of:
feature.rst,bugfix.rst,doc.rst,deprecation.rst,removal.rst.
Thus if your issue or PR number is 1234 and this change is fixing a bug, then you would create a file
docs/changelog/1234.bugfix.rst. PRs can span multiple categories by creating multiple files (for instance, if you
added a feature and deprecated/removed the old feature at the same time, you would create
docs/changelog/1234.bugfix.rst and docs/changelog/1234.removal.rst). Likewise if a PR touches multiple
issues/PRs you may create a file for each of them with the same contents and towncrier will deduplicate them.
Contents of a NEWS entry¶
The contents of this file are reStructuredText formatted text that will be used as the content of the news file entry. You do not need to reference the issue or PR numbers here as towncrier will automatically add a reference to all of the affected issues when rendering the news file.
In order to maintain a consistent style in the changelog.rst file, it is preferred to keep the news entry to the
point, in sentence case, shorter than 120 characters and in an imperative tone – an entry should complete the sentence
This change will …. In rare cases, where one line is not enough, use a summary line in an imperative tone followed
by a blank line separating it from a description of the feature/change in one or more paragraphs, each wrapped at 120
characters. Remember that a news entry is meant for end users and should only contain details relevant to an end user.
Choosing the type of NEWS entry¶
A trivial change is anything that does not warrant an entry in the news file. Some examples are: code refactors that
don’t change anything as far as the public is concerned, typo fixes, white space modification, etc. To mark a PR as
trivial a contributor simply needs to add a randomly named, empty file to the news/ directory with the extension of
.trivial.
Becoming a maintainer¶
If you want to become an official maintainer, start by helping out. As a first step, we welcome you to triage issues on virtualenv’s issue tracker. virtualenv maintainers provide triage abilities to contributors once they have been around for some time and contributed positively to the project. This is optional and highly recommended for becoming a virtualenv maintainer. Later, when you think you’re ready, get in touch with one of the maintainers and they will initiate a vote among the existing maintainers.
Note
Upon becoming a maintainer, a person should be given access to various virtualenv-related tooling across multiple platforms. These are noted here for future reference by the maintainers:
GitHub Push Access
PyPI Publishing Access
CI Administration capabilities
ReadTheDocs Administration capabilities
Maintainer access¶
Current and backup maintainers turn on two-factor authentication for GitHub and PyPI, with a security key or passkey where the service supports one. We review access once a year and whenever a maintainer steps back. When someone steps back, we remove their GitHub push access, PyPI publishing access, CI administration and ReadTheDocs administration, and rotate every secret they could read. New commit access still goes through the maintainer vote described above.
Secrets and credentials¶
The project stores two secrets, the client ID and the private key of the virtualenv-release GitHub App, as RELEASE_APP_CLIENT_ID and RELEASE_APP_PRIVATE_KEY
in the release deployment environment. The push job of pre-release.yaml and the publish job of release.yaml
name that environment, and no other job can read them. Each of the two jobs trades the key for an App token that expires
within an hour and can write the contents of one repository, virtualenv or get-virtualenv.
The rest of the release needs no stored secret. PyPI accepts uploads through trusted publishing, Sigstore signs the attestations with the workflow’s OIDC identity, and
the other jobs use their own GITHUB_TOKEN, which the repository makes read-only unless the job asks for more. Do not
create a PyPI API token or a personal access token for the project.
We follow these rules for the two secrets:
Keep them in the
releaseenvironment, and out of the repository, workflow files, logs and repository-wide secrets. Secret scanning with push protection rejects pushes that contain a known token format.Repository admins manage the environment secrets, and the App’s owner manages its keys.
Generate a new private key, store it in
RELEASE_APP_PRIVATE_KEYand delete the old key from the App settings at the yearly access review, when a maintainer steps back, and after a suspected leak, following the incident response plan.
Current maintainers¶
Rahul Devikar, who covers when Bernát is unavailable
Backup maintainer¶
Paul Moore, who steps in if both current maintainers are away
Previous maintainers¶
Roadmap¶
This roadmap covers October 2026 to September 2027; Bernát Gábor last updated it on 2026-09-26. It lists work that follows from a published policy or waits on an outside decision, and it gives no release dates. We leave out work that an open pull request delivers without waiting on anyone else.
What we plan to do¶
Add CPython 3.16 and drop CPython 3.9 under the support policy. CPython 3.9 reached end of life in October 2025, so the drop can happen from May 2027; it also retires the
_virtualenv.pthdistutils hook, which only 3.9 environments still receive (#3181).Keep releasing on the pip and setuptools cadence that Release describes.
Record
python-versioninpyvenv.cfg(PEP 838) once the PEP reaches Accepted status, restarting from the closed PR #3193. PEP 832 support shipped in PR #3204.
What we do not plan to do¶
Backport fixes to older release lines; see supported versions.
Break the command line, the Python API or the plugin API, per the backwards-compatible release policy in the same section.
Bring back Python versions that the support policy has dropped.
Write files outside the environment it creates, such as a project-level
.gitignore(#3237).Install, resolve or lock project dependencies. virtualenv creates and seeds the environment; pip, uv and similar tools manage what goes into it.