VIRTUALENV(1) virtualenv VIRTUALENV(1)

virtualenv - virtualenv 21.14.5 [image: Latest version on PyPI] [image]
https://pypi.org/project/virtualenv/#history[image: PyPI - Implementation] [image] [image: PyPI - Python Version] [image] [image: Documentation status] [image]
https://virtualenv.pypa.io[image: Discord] [image]
https://discord.gg/pypa[image: PyPI - Downloads] [image]
https://pypistats.org/packages/virtualenv[image: PyPI - License] [image]
https://opensource.org/licenses/MIT[image: Open issues] [image]
https://github.com/pypa/virtualenv/issues[image: Open pull requests] [image]
https://github.com/pypa/virtualenv/pulls[image: Package popularity] [image]
https://pypistats.org/packages/virtualenv

virtualenv is a tool to create isolated Python environments. Since Python 3.3, a subset of it has been integrated into the standard library under the venv module. For how virtualenv compares to the stdlib venv module, see Explanation <>.

Tutorials - Learn by doing

•
Getting started <> — Create your first virtual environment and learn the basic workflow

How-to guides - Solve specific problems

  • Install virtualenv <> — Install virtualenv on your system
  • Use virtualenv <> — Select Python versions, activate environments, configure defaults, and use from Python code
  • Verify a virtualenv release <> — Check that a downloaded release came from the virtualenv release workflow

Reference - Technical information

  • Compatibility <> — Supported Python versions and operating systems
  • Command line <> — Command line options and flags
  • Environment layout <> — Names the interpreter answers to inside an environment
  • Generated files <> — Files written inside and beside a created environment
  • Python <> — Programmatic Python API reference
  • Release artifacts <> — Files, attestations and SBOMs each release publishes

Explanation - Understand the concepts

•
Explanation <> — How virtualenv works under the hood and why it exists

Extensions

•
Plugins <> — Extend virtualenv with custom creators, seeders, and activators

Several tools build on virtualenv to provide higher-level workflows:

Learn more about virtualenv from these community resources:

Getting started

This tutorial will teach you the basics of virtualenv through hands-on practice. You'll create your first virtual environment, install packages, and learn how to manage project dependencies.

Before starting this tutorial, you need:

Let's create a virtual environment called myproject:

$ virtualenv myproject
created virtual environment CPython3.13.2.final.0-64 in 200ms
  creator CPython3Posix(dest=/home/user/myproject, clear=False, no_vcs_ignore=False, venv_redirect=None, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, via=copy, app_data_dir=/home/user/.cache/virtualenv)
  activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

This creates a new directory called myproject containing a complete, isolated Python environment with its own copy of Python, pip, and other tools.

pyvenv.cfg inside it records which Python the environment came from:

$ grep python-version myproject/pyvenv.cfg
python-version = 3.13

That key tells an editor or a type checker which language version your code targets.

The interpreter answers to several names, so a script expecting any of them keeps working inside the environment:

$ ls myproject/bin/python*
myproject/bin/python  myproject/bin/python3  myproject/bin/python3.13

When you create the environment inside a project folder, one with a pyproject.toml, virtualenv writes a .venv redirect file naming it:

$ cat .venv
myproject

Editors and type checkers read that file to find the environment, so they can offer the right interpreter before you activate anything. A second environment in the same folder leaves the redirect as it is; pass --venv-redirect to point it at the new one, or --no-venv-redirect to write nothing. The redirect is provisional and may change as PEP 832 evolves. Environment layout <> lists every interpreter name, and Generated files <> covers every file.

To use your virtual environment, you can activate it. The activation command differs by platform: [Linux/macOS]

$ source myproject/bin/activate
[Windows (PowerShell)]
PS> .\myproject\Scripts\Activate.ps1
[Windows (CMD)]
C:\> .\myproject\Scripts\activate.bat

After activation, your prompt changes to show the active environment:

(myproject) $

You can verify that Python is now running from inside the virtual environment: [Linux/macOS]

(myproject) $ which python
/home/user/myproject/bin/python
[Windows (PowerShell)]
(myproject) PS> where.exe python
C:\Users\user\myproject\Scripts\python.exe
[Windows (CMD)]
(myproject) C:\> where.exe python
C:\Users\user\myproject\Scripts\python.exe

With the environment activated, install a package using pip:

(myproject) $ pip install requests
Collecting requests
  Using cached requests-2.32.3-py3-none-any.whl (64 kB)
Installing collected packages: requests
Successfully installed requests-2.32.3

Verify that the package is installed only inside your virtual environment:

(myproject) $ python -c "import requests; print(requests.__file__)"
/home/user/myproject/lib/python3.13/site-packages/requests/__init__.py

The path shows that requests is installed in the virtual environment, not in your system Python.

When you're done working in the virtual environment, deactivate it:

(myproject) $ deactivate
$

The prompt returns to normal, and Python commands now use your system Python again.

Use without activation

Activation is a convenience, not a requirement. You can run any executable from the virtual environment directly by using its full path: [Linux/macOS]

$ myproject/bin/python -c "import sys; print(sys.prefix)"
/home/user/myproject
$ myproject/bin/pip install httpx
[Windows (PowerShell)]
PS> .\myproject\Scripts\python.exe -c "import sys; print(sys.prefix)"
C:\Users\user\myproject
PS> .\myproject\Scripts\pip.exe install httpx
[Windows (CMD)]
C:\> .\myproject\Scripts\python.exe -c "import sys; print(sys.prefix)"
C:\Users\user\myproject
C:\> .\myproject\Scripts\pip.exe install httpx

This is especially useful in scripts, CI pipelines, and automation where modifying the shell environment is unnecessary.

Now let's apply what you've learned to a real project workflow:

$ mkdir myapp && cd myapp
$ virtualenv venv
$ source venv/bin/activate  # or use the appropriate command for your platform
(venv) $ pip install flask requests
(venv) $ pip freeze > requirements.txt

The requirements.txt file now contains your project's dependencies:

blinker==1.9.0
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
flask==3.1.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.5
MarkupSafe==3.0.2
requests==2.32.3
urllib3==2.3.0
werkzeug==3.1.3

This file lets you recreate the exact environment later. Let's test this:

(venv) $ deactivate
$ rm -rf venv
$ virtualenv venv
$ source venv/bin/activate
(venv) $ pip install -r requirements.txt

All packages are reinstalled exactly as before. Here's the complete workflow: [graph].SS What you learned

In this tutorial, you learned how to:

  • Create a virtual environment with virtualenv.
  • Activate and deactivate virtual environments on different platforms.
  • Install packages in isolation from your system Python.
  • Save project dependencies with pip freeze.
  • Reproduce environments using requirements.txt.
  • Let editors find your environment through the .venv redirect file.

Now that you understand the basics, explore these topics:

  • Use virtualenv <> for selecting specific Python versions, configuring defaults, and advanced usage patterns.
  • Explanation <> for understanding how virtualenv works under the hood and how it compares to venv.
  • Command line <> for all available command line options and flags.
  • Verify a virtualenv release <> to check that the virtualenv you downloaded came from its release workflow.
  • Generated files <> for every file virtualenv writes inside and beside an environment.

virtualenv is a command-line tool, so it should be installed in an isolated environment rather than into your system Python. Pick the method that fits your setup:

  • uv https://docs.astral.sh/uv/ -- fast, modern Python package manager. Use this if you already have uv or are starting fresh.
  • pipx https://pipx.pypa.io/stable/ -- installs Python CLI tools in isolated environments. Use this if you already have pipx set up.
  • pip https://pip.pypa.io/stable/ -- the standard Python package installer. Use --user to avoid modifying system packages. May not work on distributions with externally-managed Python environments.
  • zipapp https://docs.python.org/3/library/zipapp.html -- a self-contained executable requiring no installation. Use this in CI or environments where you cannot install packages.
[graph][uv] Install virtualenv as a uv tool https://docs.astral.sh/uv/concepts/tools/:
$ uv tool install virtualenv

Install the development version:

$ uv tool install git+https://github.com/pypa/virtualenv.git@main
[pipx] Install virtualenv using pipx https://pipx.pypa.io/stable/:
$ pipx install virtualenv

Install the development version:

$ pipx install git+https://github.com/pypa/virtualenv.git@main
[pip] Install virtualenv using pip https://pip.pypa.io/stable/:
$ python -m pip install --user virtualenv

Install the development version:

$ python -m pip install git+https://github.com/pypa/virtualenv.git@main

Warning:

Some Linux distributions use system-managed Python environments. If you encounter errors about externally-managed environments, use uv tool or pipx instead.
[zipapp] Download the zipapp file and run it directly:
$ python virtualenv.pyz --help

Download the latest version from https://bootstrap.pypa.io/virtualenv.pyz or a specific version from https://bootstrap.pypa.io/virtualenv/x.y/virtualenv.pyz.

Each GitHub release also attaches the zipapp with a provenance bundle; Verify a virtualenv release <> shows how to check it.

Check the installed version:

$ virtualenv --version

See Compatibility <> for supported Python versions.

By default, virtualenv uses the same Python version it runs under. Override this with --python or -p.

Specify a Python version by name or version number:

$ virtualenv -p python3.14 venv
$ virtualenv -p 3.10 venv
$ virtualenv -p pypy3 venv
$ virtualenv -p rustpython venv

Use PEP 440 https://peps.python.org/pep-0440/#version-specifiers version specifiers to match Python versions:

$ virtualenv --python ">=3.12" venv
$ virtualenv --python "~=3.11.0" venv
$ virtualenv --python "cpython>=3.10" venv
  • >=3.12 -- any Python 3.12 or later.
  • ~=3.11.0 -- compatible release, equivalent to >=3.11.0, <3.12.0 (any 3.11.x patch).
  • cpython>=3.10 -- restrict to CPython implementation, 3.10 or later.

Create an environment with free-threading Python https://docs.python.org/3/howto/free-threading-python.html:

$ virtualenv -p 3.13t venv

On machines that support multiple architectures — such as Apple Silicon (arm64 + x86_64 via Rosetta) or Windows on ARM — you can request a specific CPU architecture by appending it to the spec string:

$ virtualenv -p cpython3.12-64-arm64 venv
$ virtualenv -p 3.11-64-x86_64 venv

Cross-platform aliases are normalized automatically, so amd64 and x86_64 are treated as equivalent, as are aarch64 and arm64. If omitted, any architecture matches (preserving existing behavior).

Specify the full path to a Python interpreter:

$ virtualenv -p /usr/bin/python3.9 venv

Use --try-first-with to provide a hint about which Python to check first. Unlike --python, this is a hint rather than a rule. The interpreter at this path is checked first, but only used if it matches the --python constraint.

$ virtualenv --python ">=3.10" --try-first-with /usr/bin/python3.9 venv

In this example, /usr/bin/python3.9 is checked first but rejected because it does not satisfy the >=3.10 constraint.

virtualenv automatically resolves shims from pyenv https://github.com/pyenv/pyenv, mise https://mise.jdx.dev/, and asdf https://asdf-vm.com/ to the real Python binary. Set the active Python version using any of the standard mechanisms and virtualenv will discover it:

$ pyenv local 3.12.0
$ virtualenv venv  # uses pyenv's 3.12.0, not the system Python
$ PYENV_VERSION=3.11.0 virtualenv venv  # uses 3.11.0

This also works with mise and asdf:

$ mise use python@3.12
$ virtualenv venv

No additional configuration is required. See Explanation <> for details on how shim resolution works.

Activate the environment to modify your shell's PATH and environment variables. [Bash/Zsh]

$ source venv/bin/activate
[Fish]
$ source venv/bin/activate.fish
[PowerShell]
PS> .\venv\Scripts\Activate.ps1

Note:

If you encounter an execution policy error, run Set-ExecutionPolicy RemoteSigned to allow local scripts.
[CMD]
> .\venv\Scripts\activate.bat
[Nushell]
$ overlay use venv/bin/activate.nu
[Xonsh]
@ source venv/bin/activate.xsh

Exit the virtual environment:

$ deactivate

Use without activation

Use the environment without activating it by calling executables with their full paths:

$ venv/bin/python script.py
$ venv/bin/pip install package

python, python3 and the versioned name reach the same interpreter, and on Windows the executables live in venv\Scripts under .exe names. Reach for pythonw.exe there when you want a process without a console window. Environment layout <> lists every name.

Set a custom prompt prefix:

$ virtualenv --prompt myproject venv

Disable the prompt modification by setting the VIRTUAL_ENV_DISABLE_PROMPT environment variable.

Access the prompt string via the VIRTUAL_ENV_PROMPT environment variable.

Activate the environment from within a running Python process using activate_this.py. This modifies sys.path and environment variables in the current process so that subsequent imports resolve from the virtual environment.

import runpy
runpy.run_path("venv/bin/activate_this.py")

A common use case is web applications served by a system-wide WSGI server (such as mod_wsgi or uWSGI) that need to load packages from a virtual environment:

import runpy
from pathlib import Path
runpy.run_path(str(Path("/var/www/myapp/venv/bin/activate_this.py")))
from myapp import create_app  # noqa: E402
application = create_app()

Use a configuration file to set default options for virtualenv.

The configuration file is named virtualenv.ini and located in the platformdirs app config directory. Run virtualenv --help to see the exact location for your system.

Override the location with the VIRTUALENV_CONFIG_FILE environment variable.

Derive configuration keys from command-line options by stripping leading - and replacing remaining - with _:

[virtualenv]
python = /opt/python-3.14/bin/python

Specify multiple values on separate lines:

[virtualenv]
extra_search_dir =
    /path/to/dists
    /path/to/other/dists

Set options using environment variables with the VIRTUALENV_ prefix and uppercase key names:

$ export VIRTUALENV_PYTHON=/opt/python-3.14/bin/python

For multi-value options, separate values with commas or newlines.

Set the VIRTUALENV_OVERRIDE_APP_DATA environment variable to override the default app-data cache directory location.

The periodic update checks for newer seed wheels by fetching https://pypi.org/pypi/<distribution>/json. The request is made with full TLS verification and, if the verified request fails, virtualenv now skips the update rather than downgrading the connection. Set VIRTUALENV_PERIODIC_UPDATE_INSECURE=1 to restore the previous behavior and retry with an unverified SSL context. This is an escape hatch for hosts with broken trust stores; do not set it on hosts you do not control.

Options are resolved in this order (highest to lowest priority): [graph].SS Make environments discoverable

In a project folder, one holding a pyproject.toml, virtualenv points a .venv redirect file at the first environment you create there, so editors and type checkers can find it without an activated shell. See PEP 832 https://peps.python.org/pep-0832/ for the format and Environment discovery <#environment-discovery> for the reasoning. The feature is provisional while the PEP is a draft, so a minor or patch release may change it in backward incompatible ways.

The redirect names the first environment you created in the project:

$ virtualenv py313 --python 3.13
$ virtualenv py314 --python 3.14
$ cat .venv
py313

Pass --venv-redirect to make another environment the default, or to write the redirect outside a project folder:

$ virtualenv py314 --python 3.14 --venv-redirect
$ cat .venv
py314

virtualenv leaves a .venv folder alone, and a redirect pointing at an environment it did not create. Delete the .venv if you want virtualenv to take it over.

Set the choice in the configuration file or an environment variable. no_venv_redirect = true stops the redirect:

[virtualenv]
no_venv_redirect = true
$ export VIRTUALENV_NO_VENV_REDIRECT=1

venv_redirect = true or VIRTUALENV_VENV_REDIRECT=1 writes it for every environment, as --venv-redirect does. A flag on the command line beats both, so virtualenv env --venv-redirect writes the file for one run even with no_venv_redirect = true set.

The redirect holds a path relative to its own folder, so you can commit it when the environment location is the same for everyone on the project, such as a containerized setup. Leave .venv in .gitignore when developers pick their own paths.

Update the embedded wheel files to the latest versions:

$ virtualenv --upgrade-embed-wheels

Use custom wheel files from a local directory:

$ virtualenv --extra-search-dir /path/to/wheels venv

Download the latest versions of seed packages from PyPI:

$ virtualenv --download venv

Disable automatic periodic updates of seed packages:

$ virtualenv --no-periodic-update venv

Patch the virtualenv.seed.wheels.embed module and set PERIODIC_UPDATE_ON_BY_DEFAULT to False to disable periodic updates by default. See Explanation <> for implementation details.

Read python-version out of pyvenv.cfg rather than running the environment's interpreter, which costs a subprocess and fails when the base Python has been removed:

from configparser import ConfigParser
from pathlib import Path
parser = ConfigParser()
parser.read_string("[cfg]\n" + Path("env/pyvenv.cfg").read_text(encoding="utf-8"))
parser["cfg"]["python-version"]  # '3.14'

pyvenv.cfg has no section header, hence the prefix. The key holds the feature release only; reach for version when you need the patch level too.

Call virtualenv from Python code using the cli_run function:

from virtualenv import cli_run
cli_run(["venv"])

Pass options as list elements:

cli_run(["-p", "python3.14", "--without-pip", "myenv"])

Use the returned session object to access environment details:

result = cli_run(["venv"])
print(result.creator.dest)  # path to created environment
print(result.creator.exe)  # path to python executable

Use session_via_cli to describe the environment without creating it:

from virtualenv import session_via_cli
session = session_via_cli(["venv"])
# inspect session.creator, session.seeder, session.activators

See Python <> for complete API documentation.

The release.yaml workflow in pypa/virtualenv https://github.com/pypa/virtualenv builds and publishes every release. The steps below check that a file you downloaded came out of that workflow, and show you what the release bundles. The examples use release 21.12.1; replace it with the version you have. Release artifacts <> lists every file a release publishes, and Release integrity <#release-integrity> covers what these checks prove.

You need uv https://docs.astral.sh/uv/, whose uvx runs pip and pypi-attestations https://pypi.org/project/pypi-attestations/ without installing them, the GitHub CLI https://cli.github.com for the GitHub release assets and attestations, and jq https://jqlang.org to read the SBOMs.

Download the wheel and the sdist without installing them:

$ uvx pip download virtualenv==21.12.1 --no-deps --dest .
$ uvx pip download virtualenv==21.12.1 --no-deps --no-binary :all: --dest .

Check each file against the attestation PyPI stores for it, one file per call:

$ uvx pypi-attestations verify pypi --repository https://github.com/pypa/virtualenv virtualenv-21.12.1-py3-none-any.whl
OK: virtualenv-21.12.1-py3-none-any.whl
$ uvx pypi-attestations verify pypi --repository https://github.com/pypa/virtualenv virtualenv-21.12.1.tar.gz
OK: virtualenv-21.12.1.tar.gz

A modified file fails with subject does not match distribution digest, and a file signed by another repository fails with provenance was signed by repository .... To check the copy on PyPI without downloading it first, prefix the file name with pypi:, as in pypi:virtualenv-21.12.1-py3-none-any.whl.

Releases from 21.11.0 on are immutable https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases: GitHub signs a release attestation that binds the tag to its commit and to the SHA-256 of every asset, and refuses any later change to the tag or the assets. Check that attestation:

$ gh release verify 21.12.1 --repo pypa/virtualenv
Resolved tag 21.12.1 to sha1:befec5eae075d1c4cb00a41d0c72bcdb91bf4586
Loaded attestation from GitHub API
✓ Release 21.12.1 verified!

Then check a file you downloaded against the digests the attestation lists. The command hashes the local file, so it works for a copy under any name:

$ gh release download 21.12.1 --repo pypa/virtualenv --pattern virtualenv.pyz
$ gh release verify-asset 21.12.1 virtualenv.pyz --repo pypa/virtualenv
✓ Verification succeeded! virtualenv.pyz is present in release 21.12.1

GitHub signs the release attestation itself, so it shows the assets did not change after publication. It does not name the workflow that built them; the provenance check below does.

Download the zipapp and its provenance bundle from the GitHub release, then verify one against the other:

$ gh release download 21.12.1 --repo pypa/virtualenv --pattern virtualenv.pyz --pattern virtualenv.pyz.intoto.jsonl
$ gh attestation verify virtualenv.pyz --repo pypa/virtualenv --bundle virtualenv.pyz.intoto.jsonl \
    --signer-workflow pypa/virtualenv/.github/workflows/release.yaml

Add --source-ref refs/tags/21.12.1 to also require that the build ran from the 21.12.1 tag. Without --bundle, gh fetches the attestation from GitHub instead of reading the local file.

The zipapp at https://bootstrap.pypa.io/virtualenv.pyz comes from pypa/get-virtualenv https://github.com/pypa/get-virtualenv and can trail the latest release. python virtualenv.pyz --version shows which release you have. Verify it with gh release verify-asset for that release, or with gh attestation verify as above, leaving out --bundle. Releases before 21.7.11 have neither a release attestation nor a provenance bundle; compare the file's SHA-256 with the digest GitHub lists for the release asset instead:

$ gh release view 21.7.10 --repo pypa/virtualenv --json assets --jq '.assets[] | .name + " " + .digest'
virtualenv.pyz sha256:06ee4ea84517e9b8565f7ee81c064d2de5e83dad3874d2babd7b94b2b40c4595
$ shasum -a 256 virtualenv.pyz
06ee4ea84517e9b8565f7ee81c064d2de5e83dad3874d2babd7b94b2b40c4595  virtualenv.pyz

The wheel carries a CycloneDX https://cyclonedx.org SBOM that lists the pip and setuptools wheels virtualenv bundles, with their hashes, licenses and the Python versions each one seeds. Extract it from the wheel:

$ python -m zipfile --extract virtualenv-21.12.1-py3-none-any.whl wheel
$ jq -r '.components[] | select(.hashes) | "\(.name) \(.version) Python \([.properties[] | select(.name == "virtualenv:seeded-for-python").value] | join(","))"' \
    wheel/virtualenv-21.12.1.dist-info/sboms/virtualenv.cdx.json
pip 26.0.1 Python 3.9
pip 26.2.1 Python 3.10,3.11,3.12,3.13,3.14,3.15,3.16
setuptools 82.0.1 Python 3.9
setuptools 84.0.0 Python 3.10,3.11,3.12,3.13,3.14,3.15,3.16

An installed virtualenv keeps the same file in its .dist-info directory. Print its path with the Python that runs virtualenv:

$ python -c "import importlib.metadata as m; print(next(f.locate() for f in m.files('virtualenv') if f.name == 'virtualenv.cdx.json'))"

The release workflow attests the SBOM against the wheel and the sdist. Check that attestation exists and was signed by the release workflow:

$ gh attestation verify virtualenv-21.12.1-py3-none-any.whl -R pypa/virtualenv --predicate-type https://cyclonedx.org/bom

To confirm the attested SBOM matches the one inside the wheel, save the attested copy and compare the two. diff prints nothing when they match:

$ gh attestation verify virtualenv-21.12.1-py3-none-any.whl -R pypa/virtualenv --predicate-type https://cyclonedx.org/bom \
    --format json --jq '.[0].verificationResult.statement.predicate' > attested.cdx.json
$ diff <(jq -S . attested.cdx.json) <(jq -S . wheel/virtualenv-21.12.1.dist-info/sboms/virtualenv.cdx.json)

Releases from 21.11.0 on also attach the SBOM as virtualenv.cdx.json and an SPDX 2.3 rendering of it as virtualenv.spdx.json, and attest the SPDX document against the wheel and the sdist as well. The CycloneDX asset must match the SBOM in the wheel, and the SPDX asset what the release attested:

$ gh release download 21.12.1 --repo pypa/virtualenv --pattern virtualenv.cdx.json --pattern virtualenv.spdx.json
$ unzip -p virtualenv-21.12.1-py3-none-any.whl '*.dist-info/sboms/virtualenv.cdx.json' | cmp - virtualenv.cdx.json
$ gh attestation verify virtualenv-21.12.1-py3-none-any.whl -R pypa/virtualenv --predicate-type https://spdx.dev/Document/v2.3 \
    --format json --jq '.[0].verificationResult.statement.predicate' > attested.spdx.json
$ diff <(jq -S . attested.spdx.json) <(jq -S . virtualenv.spdx.json)

Releases from 21.11.0 on describe the zipapp in its own CycloneDX SBOM. The zipapp carries it at its root, the release attaches it as virtualenv.pyz.cdx.json, and GitHub attests it against virtualenv.pyz. Check all three agree:

$ gh release download 21.12.1 --repo pypa/virtualenv --pattern virtualenv.pyz --pattern virtualenv.pyz.cdx.json
$ unzip -p virtualenv.pyz virtualenv.pyz.cdx.json | cmp - virtualenv.pyz.cdx.json
$ gh attestation verify virtualenv.pyz -R pypa/virtualenv --predicate-type https://cyclonedx.org/bom \
    --format json --jq '.[0].verificationResult.statement.predicate' > attested.pyz.cdx.json
$ diff <(jq -S . attested.pyz.cdx.json) <(jq -S . virtualenv.pyz.cdx.json)

List the distributions the zipapp bundles and the Python versions that load each one:

$ jq -r '.components[] | "\(.name) \(.version) \([.properties[] | select(.name == "virtualenv:loaded-for-python").value] | join(","))"' \
    virtualenv.pyz.cdx.json
virtualenv 21.12.1
distlib 0.4.3 3.14,3.13,3.12,3.11,3.10,3.9,3.8
filelock 3.19.1 3.9,3.8
filelock 3.32.6 3.14,3.13,3.12,3.11,3.10
platformdirs 4.11.8 3.14,3.13,3.12,3.11,3.10
platformdirs 4.4.0 3.9,3.8
python-discovery 1.6.0 3.14,3.13,3.12,3.11,3.10,3.9,3.8
typing_extensions 4.16.0 3.10,3.9,3.8

The release builds with SOURCE_DATE_EPOCH set to the commit time of the release tag, so rebuilding the tag yields the same sdist, byte for byte:

$ git clone --branch 21.12.1 https://github.com/pypa/virtualenv
$ cd virtualenv
$ SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) uv build --sdist --out-dir rebuild .
$ cmp rebuild/virtualenv-21.12.1.tar.gz ../virtualenv-21.12.1.tar.gz

cmp prints nothing when the files match.

The wheel of a release from 21.11.0 on rebuilds byte for byte too, on any operating system and architecture, once the Python patch version and the build backend versions match the ones the release used. Its SBOM lists both, so read them from the published wheel and pass them to the build:

$ unzip -p ../virtualenv-21.12.1-py3-none-any.whl '*.dist-info/sboms/virtualenv.cdx.json' > published.cdx.json
$ jq -r '.metadata.tools.components[] | select(.type == "platform") | .version' published.cdx.json
3.14.7
$ jq -r '.metadata.tools.components[] | select(.purl // "" | startswith("pkg:pypi/")) | "\(.name)==\(.version)"' \
    published.cdx.json > build-constraints.txt
$ SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) uv build --wheel --python 3.14.7 \
    --build-constraint build-constraints.txt --out-dir rebuild .
$ cmp rebuild/virtualenv-21.12.1-py3-none-any.whl ../virtualenv-21.12.1-py3-none-any.whl

Releases after 21.13.0 also pin the backend in tasks/release-requirements.txt, so on those tags --build-constraints tasks/release-requirements.txt --require-hashes replaces the file read from the SBOM. Build from a git checkout, since the SBOM records the source commit and an sdist does not carry it. Wheels up to 21.10.0 recorded the machine that built them in the SBOM, so a rebuild of those differs in the SBOM and in RECORD, which holds the SBOM's hash.

The zipapp of a release from 21.11.0 on rebuilds byte for byte as well. The pylock.zipapp.toml https://github.com/pypa/virtualenv/blob/main/pylock.zipapp.toml lock at the tag pins each distribution it bundles by version and SHA-256, so only the build tools can drift. Two SBOMs list them: the zipapp SBOM holds the tools of the tox environment that assembles the zipapp, and the wheel SBOM inside the zipapp holds the build backend for that wheel. Constrain both, and run on the CPython version the zipapp SBOM lists:

$ jq -r '.metadata.tools.components[] | select(.purl // "" | startswith("pkg:pypi/")) | "\(.name)==\(.version)"' \
    ../virtualenv.pyz.cdx.json > zipapp-constraints.txt
$ unzip -p ../virtualenv.pyz 'virtualenv-*.dist-info/sboms/virtualenv.cdx.json' \
    | jq -r '.metadata.tools.components[] | select(.purl // "" | startswith("pkg:pypi/")) | "\(.name)==\(.version)"' \
    > wheel-constraints.txt
$ UV_CONSTRAINT=$PWD/zipapp-constraints.txt PIP_BUILD_CONSTRAINT=$PWD/wheel-constraints.txt \
    SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) uvx --with tox-uv tox r -e zipapp -x 'env.zipapp.pass_env+=PIP_BUILD_CONSTRAINT'
$ cmp virtualenv.pyz ../virtualenv.pyz

The release runs tox with the tox-uv plugin as well. The plugin passes UV_* variables into the environment, and the -x override adds PIP_BUILD_CONSTRAINT for the pip wheel call that builds the virtualenv wheel inside the zipapp.

Releases after 21.13.0 pin those build tools and the SBOM tools in tasks/release-requirements.txt at the tag, so a rebuild of those takes its constraints from that file and needs neither SBOM.

virtualenv works with the following Python interpreter implementations. Only the latest patch version of each minor version is fully supported; previous patch versions work on a best effort basis.

3.15 >= python_version >= 3.9

3.11 >= python_version >= 3.9

24.1 and later (Linux and macOS only).

Experimental support (Linux, macOS, and Windows). RustPython https://github.com/RustPython/RustPython implements Python 3.14.

  • New versions are added close to their release date, typically during the beta phase.
  • Old versions are dropped 18 months after CPython EOL https://devguide.python.org/versions/, giving users plenty of time to migrate.

Major version support changes:

  • 21.5.0 (2026-06-13): dropped support for running under and creating environments for Python 3.8 and earlier.
  • 20.27.0 (2024-10-17): dropped support for running under Python 3.7 and earlier.
  • 20.22.0 (2023-04-19): dropped support for creating environments for Python 3.6 and earlier.
  • 20.18.0 (2023-02-06): dropped support for running under Python 3.6 and earlier.

CPython is shipped in multiple forms, and each OS repackages it, often applying some customization. The platforms listed below are tested. Unlisted platforms may work but are not explicitly supported. If you encounter issues on unlisted platforms, please open a feature request.

These Python distributions work on Linux, macOS, and Windows:

Note:

Framework builds do not support copy-based virtual environments. Use symlink or hardlink creation methods instead.

•
Windows Store https://apps.microsoft.com/search?query=python Python 3.9 and later

virtualenv is primarily a command line application. All options have sensible defaults, and there is one required argument: the name or path of the virtual environment to create.

See Use virtualenv <> for how to select Python versions, configure defaults, and use environment variables.

virtualenv [OPTIONS]

Named Arguments
--version '==SUPPRESS==' display the version of the virtualenv package and its location, then exit
--with-traceback False on failure also display the stacktrace internals of virtualenv
--read-only-app-data False use app data folder in read-only mode (write operations will fail with error)
--app-data platform specific application data folder a data folder used as cache by the virtualenv
--reset-app-data False start with empty app data folder
--upgrade-embed-wheels False trigger a manual update of the embedded wheels
verbosity ⇒ verbosity = verbose - quiet, default INFO, mapping => CRITICAL=0, ERROR=1, WARNING=2, INFO=3, DEBUG=4, NOTSET=5
-v, --verbose 2 increase verbosity
-q, --quiet 0 decrease verbosity

core ⇒ options shared across all discovery
--discovery 'builtin' interpreter discovery method; choice of: builtin
-p, --python the python executable virtualenv is installed into interpreter based on what to create environment (path/identifier/version-specifier) - by default use the interpreter where the tool is installed - first found wins. Version specifiers (e.g., >=3.12, ~=3.11.0, ==3.10) are also supported
--try-first-with [] try first these interpreters before starting the discovery

core ⇒ options shared across all creator
--creator builtin if exist, else venv create environment via; choice of: cpython3-mac-brew, cpython3-mac-framework, cpython3-posix, cpython3-win, graalpy-posix, graalpy-win, pypy3-posix, pypy3-win, rustpython-posix, rustpython-win, venv
dest directory to create virtualenv at
--clear False remove the destination directory if exist before starting (will overwrite files otherwise)
--no-vcs-ignore False don't create VCS ignore directive in the destination directory
--venv-redirect point a PEP-832 .venv redirect file next to the destination at the created environment, even outside a project folder; unset, virtualenv does so only beside a pyproject.toml with no .venv yet
--no-venv-redirect write no PEP-832 .venv redirect file
--system-site-packages False give the virtual environment access to the system site-packages dir
--symlinks True try to use symlinks rather than copies, when symlinks are not the default for the platform
--copies, --always-copy False try to use copies rather than symlinks, even when symlinks are the default for the platform

core ⇒ options shared across all seeder
--seeder 'app-data' seed packages install method; choice of: app-data, pip
--no-seed, --without-pip False do not install seed packages
--no-download, --never-download True pass to disable download of the latest pip/setuptools/wheel from PyPI
--download False pass to enable download of the latest pip/setuptools/wheel from PyPI
--extra-search-dir [] a path containing wheels to extend the internal wheel list (can be set 1+ times)
--pip 'bundle' version of pip to install as seed: embed, bundle, none or exact version
--setuptools 'none' version of setuptools to install as seed: embed, bundle, none or exact version
--no-pip False do not install pip
--no-setuptools False do not install setuptools
--no-periodic-update False disable the periodic (once every 14 days) update of the embedded wheels
app-data ⇒ options specific to seeder app-data
--symlink-app-data False symlink the python packages from the app-data folder (requires seed pip>=19.3)

core ⇒ options shared across all activators
--activators comma separated list of activators supported activators to generate - default is all supported; choice of: xonsh, bash, batch, cshell, fish, nushell, powershell, python
--prompt provides an alternative prompt prefix for this environment (value of . means name of the current working directory)

The interpreter in a created environment answers to more than one name, so a script or tool that hard-codes any of them keeps working. The names below are what the builtin creators write for CPython; PyPy, GraalPy and RustPython follow the same shape under their own executable names.

{minor} stands for the target interpreter's minor version, so python3.{minor} reads as python3.14 for a CPython 3.14 target.

bin holds one real reference to the host interpreter and links the rest to it:

Name Notes
python References the host interpreter.
python3 Alias.
python3.{minor} Alias carrying the target's minor version.
python3.{minor}t Free-threaded builds only.
host executable name Appears when the host carries another name, such as pypy3.{minor}.

Scripts holds copies rather than links, since symlinking the interpreter is unreliable there (bpo-42013 https://bugs.python.org/issue42013):

Name Notes
python.exe References the host interpreter.
python3.exe Alias.
python3 Alias without the extension.
python3.{minor}t.exe Free-threaded builds only.
pythonw.exe Runs without opening a console window.
pythonw3.exe Alias for pythonw.exe.
pythonw3.{minor}t.exe Free-threaded builds only.
host executable name Appears when the host carries another name, such as python_d.exe.

On CPython 3.13 and later python.exe holds a copy of the venvlauncher.exe redirector from the host's standard library rather than the interpreter binary, and pythonw.exe comes from venvwlauncher.exe. The redirector reads pyvenv.cfg and hands off to the interpreter in the base prefix, which is why the environment needs no copy of the DLLs. Its own file name stays out of Scripts, matching what the standard library's venv writes.

Note:

--copies makes the POSIX layout use copies instead of links. The set of names does not change.

Creating an environment writes files inside the destination folder and beside it. This page lists each one, what it holds, and how to suppress it.

pyvenv.cfg

Marks the folder as a virtual environment and points the interpreter at the Python it was built from, per PEP 405 https://peps.python.org/pep-0405/. Deleting it breaks the environment.

home = /usr/local/python-3.14/bin
implementation = CPython
python-version = 3.14
version_info = 3.14.6.final.0
version = 3.14.6
executable = /usr/local/python-3.14/bin/python3.14
command = /usr/bin/python3 -m virtualenv /home/user/env
virtualenv = 21.7.6
include-system-site-packages = false
base-prefix = /usr/local/python-3.14
base-exec-prefix = /usr/local/python-3.14
base-executable = /usr/local/python-3.14/bin/python3.14

Three keys carry the version, and they differ in precision and in who should read them:

Key Example Read it when
python-version 3.14 You want the feature release, which is what selects a wheel tag or a type-checker target. PEP 838 https://peps.python.org/pep-0838/ defines it and every tool creating an environment must write it.
version 3.14.6 You need the patch level as well. PEP 838 discourages reading it in favor of python-version.
version_info 3.14.6.final.0 You need the release level and serial. Written by virtualenv rather than by any specification.

prompt appears as an extra key when you pass --prompt. The base-* keys come from the creator and vary by creation method.

CACHEDIR.TAG

Marks the environment as regenerable cache content, following the cache directory tagging specification https://bford.info/cachedir/, so backup tools skip it. virtualenv leaves an existing file untouched.

.gitignore

Holds *, keeping the environment out of Git. Skip it with --no-vcs-ignore. virtualenv leaves an existing file untouched, and writes nothing for Mercurial, Bazaar or Subversion, none of which honor ignore files in a subdirectory.

bin / Scripts

The interpreter, the console scripts of any seeded package, and the activation scripts for each shell. Environment layout <> lists the names the interpreter answers to, and Activators <#activators> covers the shells.

.venv

A PEP 832 https://peps.python.org/pep-0832/ redirect file holding the destination folder name, which tells editors and type checkers which environment of the parent folder to use. virtualenv writes it in a folder holding a pyproject.toml and no .venv yet. Force it with --venv-redirect, skip it with --no-venv-redirect.

Warning:

This file is provisional because PEP 832 is still a draft, and virtualenv follows the PEP as it changes. A minor or patch release may change this file, --venv-redirect and --no-venv-redirect in backward incompatible ways.
py314

Format rules virtualenv follows when it writes the file:

Rule Behavior
Encoding UTF-8 without a byte order mark.
Content One line: the destination folder name, relative to the parent folder, followed by a newline.
Target The first environment created in a folder holding a pyproject.toml; with --venv-redirect, the environment created last, in any folder.
.venv folder Left alone, whether folder or symlink; virtualenv writes nothing when the destination itself is .venv.
Existing redirect Kept, unless you pass --venv-redirect and its target's pyvenv.cfg carries the virtualenv key; one that another tool wrote, or that points at a missing folder, stays either way.
Failure Logged as a warning; the environment is still created.

The primary interface to virtualenv is the command line application. However, it can also be used programmatically via the virtualenv.cli_run function and the Session class.

See Use virtualenv <> for usage examples.

Create a virtual environment given some command line interface arguments.
Session
the session object of the creation (its structure for now is experimental and might change on short notice)
Create a virtualenv session (same as cli_run, but this does not perform the creation). Use this if you just want to query what the virtual environment would look like, but not actually create it.
Session
the session object of the creation (its structure for now is experimental and might change on short notice)

The Session class represents a virtualenv creation session and provides access to the created environment's properties.

Represents a virtual environment creation session.
Create a virtual environment based on this reference interpreter.
The creator used to build the virtual environment (must be compatible with the interpreter).
The mechanism used to provide the seed packages (pip, setuptools, wheel).

Options namespace passed to plugin constructors, populated from the CLI, environment variables, and configuration files.

Set an option value and record where it came from.
Return the source that provided a given option value.
key (str https://docs.python.org/3/builtins/stdtypes.html#str) -- the option name
str https://docs.python.org/3/builtins/stdtypes.html#str | None https://docs.python.org/3/builtins/constants.html#None
the source string (e.g. "cli", "env var", "default"), or None if not tracked
The verbosity level, computed as verbose - quiet, clamped to zero.
the verbosity level, or None if neither --verbose nor --quiet has been parsed yet

A maintainer runs the pre-release workflow https://github.com/pypa/virtualenv/actions/workflows/pre-release.yaml, which writes the changelog, commits the release and pushes the version tag. That tag starts the release workflow https://github.com/pypa/virtualenv/actions/workflows/release.yaml, which builds and publishes every file below. {version} stands for the release version, such as 21.10.0. Verify a virtualenv release <> shows how to check each file, and Release integrity <#release-integrity> explains what the checks prove.

File Where Contents
virtualenv-{version}-py3-none-any.whl PyPI https://pypi.org/project/virtualenv/ the package, with its CycloneDX SBOM under .dist-info/sboms
virtualenv-{version}.tar.gz PyPI the source distribution
virtualenv.pyz GitHub release https://github.com/pypa/virtualenv/releases the zipapp: virtualenv and its runtime dependencies for every supported Python, with its own SBOM at the archive root
virtualenv.pyz.intoto.jsonl GitHub release the Sigstore bundle holding the zipapp's SLSA provenance
virtualenv.pyz.cdx.json GitHub release the zipapp's CycloneDX SBOM, the same file the zipapp carries
virtualenv.cdx.json GitHub release the wheel's CycloneDX SBOM, the same file the wheel carries
virtualenv.spdx.json GitHub release the wheel's SBOM rendered as SPDX 2.3
virtualenv.pyz https://bootstrap.pypa.io/virtualenv.pyz the latest release's zipapp
virtualenv.pyz for Python {x.y} https://bootstrap.pypa.io/virtualenv/{x.y}/virtualenv.pyz the zipapp for Python {x.y}

The bootstrap copies come from the public directory of pypa/get-virtualenv https://github.com/pypa/get-virtualenv, which the release workflow updates, and can trail the latest release. Releases published before an asset existed do not have it; gh release view {version} --repo pypa/virtualenv lists what a release carries. Releases from 21.11.0 on are immutable; GitHub rejects changes to their tag and assets after publication.

Attestation Subject Stored at
PyPI publish attestation (PEP 740 https://peps.python.org/pep-0740/), predicate https://docs.pypi.org/attestations/publish/v1 wheel, sdist https://pypi.org/integrity/virtualenv/{version}/{file}/provenance
SLSA provenance, predicate https://slsa.dev/provenance/v1 virtualenv.pyz GitHub attestations API and virtualenv.pyz.intoto.jsonl
CycloneDX SBOM, predicate https://cyclonedx.org/bom wheel, sdist GitHub attestations API, predicate equal to virtualenv.cdx.json
SPDX SBOM, predicate https://spdx.dev/Document/v2.3 wheel, sdist GitHub attestations API, predicate equal to virtualenv.spdx.json
CycloneDX SBOM, predicate https://cyclonedx.org/bom virtualenv.pyz GitHub attestations API, predicate equal to virtualenv.pyz.cdx.json
GitHub release attestation, predicate https://in-toto.io/attestation/release/v0.2 the tag's commit and every GitHub release asset, from 21.11.0 on GitHub attestations API, read by gh release verify

The release workflow signs every attestation except the last through Sigstore https://www.sigstore.dev with the identity https://github.com/pypa/virtualenv/.github/workflows/release.yaml@refs/tags/{version}. GitHub signs the release attestation when it publishes an immutable release, with the identity https://dotcom.releases.github.com.

  • Format: CycloneDX 1.6 JSON.
  • Location in the wheel: virtualenv-{version}.dist-info/sboms/virtualenv.cdx.json, following PEP 770 https://peps.python.org/pep-0770/. Installers copy it into the installed .dist-info directory. The sdist does not carry one.
  • Components: every wheel bundled under virtualenv/seed/wheels/embed, with its SHA-256, license, the packages it vendors, and a virtualenv:seeded-for-python property per Python version that receives it; plus the runtime dependencies declared in the wheel metadata, without versions, since the installer resolves those.
  • Licenses: a component whose metadata has a License-Expression carries that SPDX expression. Otherwise it lists its License field and License :: classifier names as declared, plus the SPDX ids they map to as concluded when each name maps to a single SPDX license.
  • Build record: the Python version and build backend packages that produced the wheel, the source commit, and the SOURCE_DATE_EPOCH used for timestamps. Releases up to 21.10.0 also recorded the operating system, architecture and interpreter build of the build machine.
  • First release carrying it: 21.8.1.

virtualenv.spdx.json is the wheel SBOM rendered as SPDX 2.3 JSON for tools that read only SPDX. It keeps the packages, SHA-256 hashes, declared and concluded licenses, purls and the containment, dependency and build tool relationships. It leaves out the per-file hashes, because SPDX 2.3 requires a SHA-1 for every file and wheel RECORD files hold SHA-256, and the build record, which SPDX 2.3 has no field for. Its document namespace ends in the CycloneDX serial number, so both documents name the same build.

  • Format: CycloneDX 1.6 JSON, at virtualenv.pyz.cdx.json in the zipapp's root and as a release asset.
  • Root component: pkg:generic/virtualenv.pyz@{version}.
  • Components: virtualenv, with a SHA-256 per file and the embedded pip and setuptools wheels by purl and SHA-256; and each bundled distribution, such as filelock or platformdirs, with its version, license, a virtualenv:loaded-for-python property per Python version that imports it, and a SHA-256 per file. Every file in the archive other than the SBOM appears in it.
  • Licenses: recorded the same way as in the wheel SBOM.
  • Build record: the Python version and packages of the environment that built the zipapp, and the SOURCE_DATE_EPOCH used for timestamps. Packages installed from platform-specific wheels appear without their files, which differ per operating system and architecture.

Only Python 3.9 environments receive the embedded pip 26.0.1 and setuptools 82.0.1, and both versions have published advisories that a scanner reading the SBOMs reports. pip 26.1 and setuptools 83 and later require Python 3.10, so --pip and --setuptools cannot pick a fixed version for Python 3.9; create environments for Python 3.10 or newer to avoid them.

The release sets SOURCE_DATE_EPOCH to the commit time of the tag (git log -1 --pretty=%ct). Rebuilding the tag with the same value reproduces the sdist byte for byte.

The wheel, whose SBOM hatch_build.py https://github.com/pypa/virtualenv/blob/main/hatch_build.py writes, reproduces byte for byte on any operating system and architecture when these inputs match the release:

  • the source tree, as a git checkout of the tag, since the SBOM records the commit;
  • SOURCE_DATE_EPOCH;
  • the Python patch version, which the SBOM records;
  • the versions of the build backend and its dependencies, which the SBOM lists.

Any build frontend works, since the SBOM leaves out the installer metadata a frontend writes into the build environment.

Wheels up to 21.10.0 recorded the build machine in their SBOM, so a rebuild of those differs in the SBOM and in RECORD, which holds the SBOM's hash.

The release pins the build backend and its dependencies by version and SHA-256 in tasks/release-requirements.txt https://github.com/pypa/virtualenv/blob/main/tasks/release-requirements.txt, so a checkout of the tag carries them.

The zipapp, built by tasks/make_zipapp.py https://github.com/pypa/virtualenv/blob/main/tasks/make_zipapp.py, needs the same inputs, and its entries carry SOURCE_DATE_EPOCH as their timestamp and fixed permissions. The build takes the distributions it bundles from pylock.zipapp.toml https://github.com/pypa/virtualenv/blob/main/pylock.zipapp.toml, a PEP 751 https://peps.python.org/pep-0751/ lock that pins each wheel by SHA-256, and fails on a hash mismatch. A rebuild of a release from 21.11.0 on also needs the versions of the tools that built it: the zipapp SBOM lists the packages of the build environment, and the wheel SBOM inside the zipapp lists the backend that built that wheel. Releases after 21.13.0 also pin those tools in tasks/release-requirements.txt.

This page explains the design decisions and concepts behind virtualenv. It focuses on understanding why things work the way they do.

Since Python 3.3, the standard library includes the venv module, which provides basic virtual environment creation following PEP 405 https://www.python.org/dev/peps/pep-0405/. uv https://docs.astral.sh/uv/pip/environments/ is a newer, Rust-based tool that also creates virtual environments via uv venv.

virtualenv occupies a middle ground: faster and more featureful than venv, while remaining a pure Python solution with a plugin system for extensibility.

venv virtualenv uv https://docs.astral.sh/uv/
Performance Slowest (60s+); spawns pip https://pip.pypa.io as a subprocess to seed. Fast; caches pre-built install images, subsequent creation < 1 second. Fastest; Rust implementation, milliseconds. Does not seed pip/setuptools by default.
Extensibility No plugin system. Plugin system for discovery, creation, seeding, and activation. No plugin system.
Cross-version Only the Python version it runs under. Any installed Python via auto-discovery (registry, uv-managed, PATH). Any installed or uv-managed Python.
Upgradeability Tied to Python releases. Independent via PyPI https://pypi.org/project/virtualenv/. Independent via its own release cycle.
Programmatic API Basic create() function only. Full Python API; can describe environments without creating them. Used by tox https://tox.readthedocs.io, poetry https://python-poetry.org/, pipx https://pipx.pypa.io, etc. Command line only.
Type annotations No py.typed marker; limited annotations. Fully typed with PEP 561 https://peps.python.org/pep-0561/ py.typed marker; checked by ty https://docs.astral.sh/ty/. Not applicable (Rust binary).
Best for Zero dependencies, basic needs. Plugin extensibility, programmatic API, tool compatibility (tox https://tox.readthedocs.io, virtualenvwrapper https://virtualenvwrapper.readthedocs.io). Maximum speed, already using uv for package management.
[graph].SS Supported Python versions

virtualenv distinguishes between the host interpreter (the Python running virtualenv itself) and the target interpreter (the Python for which the virtual environment is created). When --python is not specified, the host and target are the same interpreter.

virtualenv requires CPython or PyPy 3.9 or later as the host interpreter. This is enforced by requires-python >= 3.9 in the package metadata.

The target interpreter can differ from the host. virtualenv can create virtual environments for any Python interpreter it can discover on the system, provided a matching creator exists.

Implementation Platforms Notes
CPython Linux, macOS, Windows 3.9+. Free-threaded builds supported on 3.13+. Includes macOS framework, Homebrew, Microsoft Store, and Windows debug build support.
PyPy Linux, macOS, Windows 3.9+.
GraalPy Linux, macOS, Windows 24.1+. Minimal test coverage, marked experimental.
RustPython Linux, macOS, Windows Minimal test coverage, marked experimental.

Seed packages (pip, setuptools) are bundled for CPython 3.9 through 3.16. A target newer than the highest bundled version reuses the newest bundle. A target older than the oldest bundled version has no compatible bundled wheel, so the bundled seeders refuse it before the environment is created; pass --no-seed for an empty environment, or select a seeder that ships wheels for that version.

Python packaging often faces a fundamental problem: different applications require different versions of the same library. If Application A needs requests==2.25.1 but Application B needs requests==2.28.0, installing both into the global site-packages directory creates a conflict. Only one version can exist in a given location.

virtualenv solves this by creating isolated Python environments. Each environment has its own installation directories and can maintain its own set of installed packages, independent of other environments and the system Python.

virtualenv operates in two distinct phases: [graph].INDENT 0.0

virtualenv first identifies which Python interpreter to use as the template for the virtual environment. By default, it uses the same Python version that virtualenv itself is running on. You can override this with the --python flag to specify a different interpreter.
Once the target interpreter is identified, virtualenv creates the environment in five steps:
1.
Create a Python executable matching the target interpreter
2.
Install seed packages (pip, setuptools, wheel) to enable package installation
3.
Install activation scripts for various shells
4.
Create VCS ignore files (currently Git's .gitignore, skip with --no-vcs-ignore)
5.
In a project folder, point a .venv redirect file beside the environment at it so editors and type checkers can find it (PEP 832 https://peps.python.org/pep-0832/, provisional; force with --venv-redirect, skip with --no-venv-redirect)


An important design principle: virtual environments are not self-contained. A complete Python installation consists of thousands of files, and copying all of them into every virtual environment would be wasteful. Instead, virtual environments are lightweight shells that borrow most content from the system Python. They contain only what's needed to redirect Python's behavior.

pyvenv.cfg is what makes that redirection work. The interpreter reads it at startup to find its base installation, so the file is the environment rather than a description of it. That also makes it the cheapest place for other tools to learn about the environment, which is why it accumulated three keys carrying the same version at different precisions. PEP 838 https://peps.python.org/pep-0838/ settles which one to read by requiring python-version, holding the feature release and nothing more, since a wheel tag and a type-checker target both stop at the minor version. version and version_info stay for the callers that need a patch level or a release level, and Generated files <> sets out the difference.

This design has two implications:

  • Environment creation is fast because only a small number of files need to be created.
  • Upgrading the system Python might affect existing virtual environments, since they reference the system Python's standard library and binary extensions.

The Python executable in a virtual environment is effectively isolated from the one used to create it, but the supporting files are shared.

Warning:

If you upgrade your system Python, existing virtual environments will still report the old version (the version number is embedded in the Python executable itself), but they will use the new version's standard library and binary extensions. This normally works without issues, but be aware that the environment is effectively running a hybrid of old and new Python versions.

Before creating a virtual environment, virtualenv must locate a Python interpreter. The interpreter determines the virtual environment's Python version, implementation (CPython, PyPy, etc.), and architecture (32-bit or 64-bit).

The --python flag accepts several specifier formats:

An absolute or relative path to a Python executable, such as /usr/bin/python3.14 or ./python.
A string following the format {implementation}{version}{architecture}{machine} where:
  • Implementation is alphabetic characters (python means any implementation; if omitted, defaults to python).
  • Version is dot-separated numbers, optionally followed by t for free-threading builds.
  • Architecture is -64 or -32 (if omitted, means any architecture).
  • Machine is the CPU instruction set architecture, e.g. -arm64, -x86_64, -aarch64 (if omitted, means any machine). Cross-platform aliases are normalized automatically (amd64 ↔ x86_64, aarch64 ↔ arm64).

Examples:

  • python3.14.1 - Any Python implementation with version 3.14.1
  • 3 - Any Python implementation with major version 3
  • 3.13t - Any Python implementation version 3.13 with free-threading enabled
  • cpython3 - CPython implementation with major version 3
  • pypy2 - PyPy implementation with major version 2
  • cpython3.12-64-arm64 - CPython 3.12, 64-bit, ARM64 architecture
  • 3.11-64-x86_64 - Any implementation, version 3.11, 64-bit, x86_64 architecture
  • rustpython - RustPython implementation
Version constraints using PEP 440 operators:
  • >=3.12 - Any Python 3.12 or later
  • ~=3.11.0 - Compatible with Python 3.11.0
  • cpython>=3.10 - CPython 3.10 or later

When you provide a specifier, virtualenv searches for matching interpreters using this strategy: [graph].INDENT 0.0

1.
Windows Registry (Windows only): Check registered Python installations per PEP 514 https://www.python.org/dev/peps/pep-0514/.
2.
uv-managed installations: Check the UV_PYTHON_INSTALL_DIR environment variable or platform-specific uv Python directories for managed Python installations.
3.
PATH search: Search for executables on the PATH environment variable with names matching the specification.


Version managers like pyenv https://github.com/pyenv/pyenv, mise https://mise.jdx.dev/, and asdf https://asdf-vm.com/ place lightweight shim scripts on PATH that delegate to the real Python binary. When virtualenv discovers a Python interpreter by running it as a subprocess, shims may resolve to the wrong Python version (typically the system Python) because the shim's resolution logic depends on shell environment state that doesn't fully propagate to child processes.

virtualenv detects shims by checking whether the candidate executable lives in a known shim directory ($PYENV_ROOT/shims, $MISE_DATA_DIR/shims, or $ASDF_DATA_DIR/shims). When a shim is detected, virtualenv bypasses it and locates the real binary directly under the version manager's versions directory, using the active version from:

1.
The PYENV_VERSION environment variable (colon-separated for multiple versions).
2.
A .python-version file in the current directory or any parent directory.
3.
The global version file at $PYENV_ROOT/version.

This convention is shared across pyenv, mise, and asdf, so the same resolution logic works for all three.

Warning:

Virtual environments typically reference the system Python's standard library. If you upgrade the system Python, the virtual environment will report the old version (embedded in its Python executable) but will actually use the new version's standard library content. This can cause confusion when debugging version-specific behavior.

If you use a virtual environment's Python as the target for creating another virtual environment, virtualenv will detect the system Python version and create an environment matching the actual (upgraded) version, not the version reported by the virtual environment.

Creators are responsible for constructing the virtual environment structure. virtualenv supports two types of creators:

This creator delegates the entire creation process to the standard library's venv module, following PEP 405 https://www.python.org/dev/peps/pep-0405/. The venv creator requires spawning a subprocess to invoke the venv module, unless virtualenv is installed in the system Python.

The subprocess overhead can be significant, especially on Windows where process creation is expensive.

This creator means virtualenv performs the creation itself by knowing exactly which files to create and which system files to reference. The builtin creator is actually a family of specialized creators for different combinations of Python implementation (CPython, PyPy, GraalPy, RustPython) and platform (Windows, POSIX). The name builtin is an alias that selects the first available builtin creator for the target environment.

Because builtin creators don't require subprocess invocation, they're generally faster than the venv creator.

[graph]

virtualenv defaults to using the builtin creator if one is available for the target environment, falling back to the venv creator otherwise.

Whichever creator runs, the interpreter ends up reachable under several names, listed in Environment layout <>. Tools reach for different ones - a shebang may say python, a CI job may call the versioned name - and an environment offering only one of them would break the others. The builtin creators take the version-bearing names from the target interpreter and add the host executable's own file name, so a host called pypy3.11 or python_d.exe stays reachable under the name its callers already use.

Windows copies rather than symlinks, because symlinking the interpreter there is unreliable (bpo-42013 https://bugs.python.org/issue42013). From CPython 3.13 the file copied is the venvlauncher.exe redirector out of the host's standard library rather than the interpreter itself. The redirector reads pyvenv.cfg and hands off to the base prefix, which saves copying the DLLs and the standard library zip into every environment. Which name a copy carries makes no difference to where it lands, since the redirector resolves its target from a value compiled into it rather than from its own file name.

After creating the virtual environment structure, virtualenv installs seed packages that enable package management within the environment. The seed packages are:

  • pip - The package installer for Python (always installed).
  • setuptools - Package development and installation library (disabled by default on Python 3.12+).

virtualenv supports two seeding methods with dramatically different performance characteristics:

This method uses the bundled pip wheel to install seed packages by spawning a child pip process. The subprocess performs a full installation, including unpacking wheels and generating metadata. This method is reliable but slow, typically consuming 98% of the total virtual environment creation time.
This method creates reusable install images in a user application data directory. The first time you create an environment with specific seed package versions, the app-data seeder builds complete install images and stores them in the cache. Subsequent environment creations simply link or copy these pre-built images into the virtual environment's site-packages directory.

Performance comparison for creating virtual environments: [graph]

On platforms that support symlinks efficiently (Linux, macOS), the app-data seeder provides nearly instant seeding.

You can override the cache location using the VIRTUALENV_OVERRIDE_APP_DATA environment variable.

Both seeding methods require wheel files for the seed packages. virtualenv acquires wheels using a priority system: [graph].INDENT 0.0

virtualenv ships with a set of wheels bundled directly into the package. These are tested with the virtualenv release and provide a baseline set of seed packages. Different Python versions require different package versions, so virtualenv bundles multiple wheels to support its wide Python version range.
Users can manually upgrade the embedded wheels by running virtualenv with the --upgrade-embed-wheels flag. This fetches newer versions of seed packages from PyPI and stores them in the user application data directory. Subsequent virtualenv invocations will use these upgraded wheels instead of the embedded ones.

virtualenv can also perform periodic automatic upgrades (see below).

Users can specify additional directories containing wheels using the --extra-search-dir flag. This is useful in air-gapped environments or when using custom package builds.
If no suitable wheel is found in the above locations, or if the --download flag is set, virtualenv will use pip to download the latest compatible version from PyPI.


To keep the seed packages reasonably current without requiring users to manually upgrade virtualenv or run --upgrade-embed-wheels, virtualenv implements a periodic automatic update system: [graph]

The 28-day waiting period protects users from automatically adopting newly released packages that might contain bugs. The 1-hour delay after download ensures continuous integration systems don't start using different package versions mid-run, which could cause confusing test failures.

You can disable the periodic update mechanism with the --no-periodic-update flag.

Operating system distributions and package managers sometimes need to customize which seed package versions virtualenv uses. They want to align virtualenv's bundled packages with system package versions.

Distributions can patch the virtualenv.seed.wheels.embed module, replacing the get_embed_wheel function with their own implementation that returns distribution-provided wheels. If they want to use virtualenv's test suite for validation, they should also provide the BUNDLE_FOLDER, BUNDLE_SUPPORT, MIN, MAX, and OLDEST_SUPPORTED variables. OLDEST_SUPPORTED (the parsed form of MIN) sets the floor below which the bundled seeders refuse to seed, so a distribution that bundles wheels for an older Python must lower it to match.

Distributions should also consider patching virtualenv.seed.embed.base_embed.PERIODIC_UPDATE_ON_BY_DEFAULT to False, allowing the system package manager to control seed package updates rather than virtualenv's periodic update mechanism. Users can still manually request upgrades via --upgrade-embed-wheels, but automatic updates won't interfere with system-managed packages.

Activation scripts modify the current shell environment to prioritize the virtual environment's executables. This is purely a convenience mechanism - you can always use absolute paths to virtual environment executables without activating.

What activation does: [graph].INDENT 0.0

The activation script prepends the virtual environment's bin directory (Scripts on Windows) to the PATH environment variable. This ensures that when you run python, pip, or other executables, the shell finds the virtual environment's versions first.
Activation sets several environment variables:
  • VIRTUAL_ENV - Absolute path to the virtual environment directory.
  • VIRTUAL_ENV_PROMPT - The prompt prefix (the environment name or custom value from --prompt).
  • PKG_CONFIG_PATH - Modified to include the virtual environment's lib/pkgconfig directory.
By default, activation prepends the environment name to your shell prompt, typically shown as (venv) before the regular prompt. This visual indicator helps you remember which environment is active. You can customize this with the --prompt flag when creating the environment, or disable it entirely by setting the VIRTUAL_ENV_DISABLE_PROMPT environment variable.
Activation scripts also provide a deactivate command that reverses the changes, restoring your original PATH and removing the environment variables and prompt modifications.


virtualenv provides activation scripts for multiple shells:

Note:

On Windows 7 and later, PowerShell's default execution policy is Restricted, which prevents running the activate.ps1 script. You can allow locally-generated scripts to run by changing the execution policy:
Set-ExecutionPolicy RemoteSigned

Since virtualenv generates activate.ps1 locally for each environment, PowerShell considers it a local script rather than a remote one and allows execution under the RemoteSigned policy.

Remember: activation is optional. The following commands are equivalent:

# With activation
source venv/bin/activate
python script.py
deactivate
# Without activation
venv/bin/python script.py

For a deeper dive into how activation works under the hood, see Allison Kaptur's blog post There's no magic: virtualenv edition https://www.recurse.com/blog/14-there-is-no-magic-virtualenv-edition, which explains how virtualenv uses PATH and PYTHONHOME to isolate virtual environments.

virtualenv installs the pip and setuptools wheels it bundles into every environment it creates, so a tampered release would reach all of them. Each release lets you check where its files came from without trusting the maintainers' machines. Verify a virtualenv release <> walks through the checks and Release artifacts <> lists the files involved.

The release workflow holds no PyPI API token. Its publish job exchanges a short-lived GitHub OIDC token for upload rights through PyPI's trusted publishing https://docs.pypi.org/trusted-publishers/, and the publish attestation PyPI keeps for each file names that publisher: the release.yaml workflow of pypa/virtualenv in the release environment.
For each file, the workflow signs a statement that binds the file's SHA-256 to the workflow identity: repository, workflow file, tag and commit. Sigstore https://www.sigstore.dev issues the signing certificate and records it in a public transparency log. PyPI stores the statements for the wheel and sdist; GitHub stores the one for the zipapp and the SBOM attestations. Verifying a file recomputes its digest and checks it against a signed statement whose identity you name.
From 21.11.0 on, once the workflow publishes a GitHub release, GitHub refuses changes to its tag and assets and signs a release attestation over the tag's commit and each asset's SHA-256. A stolen maintainer token cannot swap the zipapp or an SBOM on a published release. The release attestation names no workflow, so pair it with the provenance check.
Provenance tells you which workflow run built a file, but you still trust that run. The release pins every timestamp to SOURCE_DATE_EPOCH, the commit time of the tag, so you can rebuild the sdist, the wheel and the zipapp from the tag yourself and compare the bytes. The SBOMs record the Python version and build tools the release used, which a rebuild must match, and a lock file pins the distributions the zipapp bundles. The SBOMs leave out the machine that ran the build, which the provenance attestation already names.
Dependency scanners find the packages a project declares, and virtualenv declares neither pip nor setuptools: it ships them as data files. The CycloneDX SBOM in the wheel names each bundled wheel with its hash, license and vendored packages, so a scanner can match them against vulnerability databases. The release also publishes it as SPDX for tools that read only that format. The zipapp gets its own SBOM because it bundles more: the runtime dependencies for every supported Python, each in the version that Python loads.

What a successful check proves:

  • The file is byte-identical to one that release.yaml in pypa/virtualenv built and signed.
  • With --source-ref or --signer-workflow, the run came from that tag and workflow file.

What it does not prove:

  • That the source at that tag is free of bugs or malicious code. Provenance ties a file to a commit; review of the commit is a separate matter.
  • That the actions and build dependencies the workflow pulled in were not compromised. The SBOM records the build environment so you can audit it after the fact.
  • That the bundled pip and setuptools have no known vulnerabilities. The SBOM lets you look them up.
  • Anything about wheels virtualenv fetches at run time. --download, --upgrade-embed-wheels and the periodic update take pip and setuptools from PyPI, outside virtualenv's attestations.
  • That you have the latest release. The bootstrap zipapp in particular can trail it.

Editors, type checkers and task runners need to find a project's environment, and they cannot count on an activated shell to tell them where it is. Launching an editor on a fresh checkout leaves it guessing. Each tool has answered that by hard-coding a search per environment manager it wants to support, which is why editor support for any new tool lags behind the tool itself.

PEP 832 https://peps.python.org/pep-0832/ gives the guess a fixed place to look: .venv in the project root, either the environment itself or a redirect file holding a single line with the path of one. virtualenv creates environments, so it writes the redirect; reading it belongs to the tools consuming it.

virtualenv is a low-level tool, and most environments it builds are not a project's default, such as scratch environments or the ones tox and nox create for their own use. By default it writes the redirect when the environment lands in a project folder, one holding a pyproject.toml, and that folder has no .venv yet:

$ ls
pyproject.toml  src
$ virtualenv env
$ virtualenv scratch
$ cat .venv
env
Later environments leave the redirect alone, so a throwaway environment next to your working one does not move your editor to it. Pass --venv-redirect to point the redirect at the new environment.
virtualenv foo in a folder without a pyproject.toml leaves the folder as it was. --venv-redirect writes the redirect there anyway.
Creating .venv itself writes nothing, and where a .venv folder or symlink exists virtualenv leaves it alone, since it already is the default.
Even with --venv-redirect, virtualenv honors the PEP's request not to overwrite a redirect another tool wrote. The file carries no marker of who wrote it, so virtualenv judges by the target: it takes over the redirect only when the pyvenv.cfg there carries the virtualenv key. A redirect written by uv, venv or you stays, and so does one whose target is gone, since it may still resolve inside a container or on another machine.
--venv-redirect and --no-venv-redirect set one choice, and a flag on the command line beats the environment variable and the configuration file, so a single run can override a standing setting in either direction.
A write failure logs a warning and leaves you with a working environment. Discovery is a convenience for other tools, so it does not get to fail the job you asked for.

An earlier draft of the PEP listed every environment in a .python-envs file. The PEP went back to a single redirect after workflow tool authors warned that tools sharing one list would overwrite each other's entries and change the default under the user. virtualenv follows the PEP, so it writes one line.

The feature is provisional. PEP 832 is still a draft whose format changed twice during review, and virtualenv tracks the PEP rather than its own earlier behavior, so a minor or patch release may change what virtualenv writes in backward incompatible ways. Pass --no-venv-redirect if you need a stable project folder until the Steering Council accepts the PEP.

  • Use virtualenv <> - Practical guides for common virtualenv tasks.
  • Command line <> - Complete CLI reference documentation.

virtualenv can be extended via plugins using Python entry points. Plugins are automatically discovered from the Python environment where virtualenv is installed, allowing you to customize how virtual environments are created, seeded, and activated.

virtualenv provides four extension points through entry point groups:

Python interpreter discovery plugins. These plugins locate and identify Python interpreters that will be used as the base for creating virtual environments.
Virtual environment creator plugins. These plugins handle the actual creation of the virtual environment structure, including copying or symlinking the Python interpreter and standard library.
Seed package installer plugins. These plugins install initial packages (like pip, setuptools, wheel) into newly created virtual environments.
Shell activation script plugins. These plugins generate shell-specific activation scripts that modify the environment to use the virtual environment.

All extension points follow a common pattern: virtualenv discovers registered entry points, builds CLI options from them, and executes the selected implementations during environment creation.

This tutorial walks through creating a simple discovery plugin that locates Python interpreters managed by pyenv.

Set up a new Python package with the following structure:

virtualenv-pyenv/
├── pyproject.toml
└── src/
    └── virtualenv_pyenv/
        └── __init__.py

In pyproject.toml, declare your plugin as an entry point under the virtualenv.discovery group:

[project]
name = "virtualenv-pyenv"
version = "0.1.0"
dependencies = ["virtualenv>=20"]
[project.entry-points."virtualenv.discovery"]
pyenv = "virtualenv_pyenv:PyEnvDiscovery"
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

In src/virtualenv_pyenv/__init__.py, implement the discovery plugin by subclassing Discover:

from __future__ import annotations
import subprocess
from argparse import ArgumentParser
from pathlib import Path
from virtualenv.config.cli.parser import VirtualEnvOptions
from virtualenv.discovery.discover import Discover
from virtualenv.discovery.py_info import PythonInfo
class PyEnvDiscovery(Discover):
    def __init__(self, options: VirtualEnvOptions) -> None:
        super().__init__(options)
        self.python_spec = options.python if options.python else "python"
    @classmethod
    def add_parser_arguments(cls, parser: ArgumentParser) -> None:
        parser.add_argument(
            "--python",
            dest="python",
            metavar="py",
            type=str,
            default=None,
            help="pyenv Python version to use (e.g., 3.11.0)",
        )
    def run(self) -> PythonInfo | None:
        try:
            result = subprocess.run(
                ["pyenv", "which", "python"],
                capture_output=True,
                text=True,
                check=True,
            )
            python_path = Path(result.stdout.strip())
            return PythonInfo.from_exe(str(python_path))
        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            raise RuntimeError(f"Failed to locate pyenv Python: {e}") from e

Install your plugin in development mode alongside virtualenv:

$ pip install -e virtualenv-pyenv/

Check that virtualenv recognizes your plugin by running:

$ virtualenv --discovery help

The output should list pyenv as an available discovery mechanism. You can now use it:

$ virtualenv --discovery=pyenv myenv
created virtual environment CPython3.11.0.final.0-64 in 234ms
  creator CPython3Posix(dest=/path/to/myenv, clear=False, no_vcs_ignore=False, venv_redirect=None, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/path)
    added seed packages: pip==23.0, setuptools==65.5.0, wheel==0.38.4
  activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

This page provides task-oriented guides for creating each type of virtualenv plugin.

Discovery plugins locate Python interpreters. Register your plugin under the virtualenv.discovery entry point group.

Implement the Discover interface:

from __future__ import annotations
from argparse import ArgumentParser
from virtualenv.config.cli.parser import VirtualEnvOptions
from virtualenv.discovery.discover import Discover
from virtualenv.discovery.py_info import PythonInfo
class CustomDiscovery(Discover):
    @classmethod
    def add_parser_arguments(cls, parser: ArgumentParser) -> None:
        parser.add_argument("--custom-opt", help="custom discovery option")
    def __init__(self, options: VirtualEnvOptions) -> None:
        super().__init__(options)
        self.custom_opt = options.custom_opt
    def run(self) -> PythonInfo | None:
        # Locate Python interpreter and return PythonInfo
        return PythonInfo.from_exe(str(self._find_python()))
    def _find_python(self) -> str:
        # Implementation-specific logic
        ...

Register the entry point:

[virtualenv.discovery]
custom = your_package.discovery:CustomDiscovery

Creator plugins build the virtual environment structure. Register under virtualenv.create.

Implement the Creator interface:

from __future__ import annotations
from argparse import ArgumentParser
from virtualenv.app_data.base import AppData
from virtualenv.config.cli.parser import VirtualEnvOptions
from virtualenv.create.creator import Creator, CreatorMeta
from virtualenv.discovery.py_info import PythonInfo
class CustomCreator(Creator):
    @classmethod
    def add_parser_arguments(
        cls,
        parser: ArgumentParser,
        interpreter: PythonInfo,
        meta: CreatorMeta,
        app_data: AppData,
    ) -> None:
        parser.add_argument("--custom-creator-opt", help="custom creator option")
    def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
        super().__init__(options, interpreter)
        self.custom_opt = options.custom_creator_opt
    def create(self) -> None:
        # Create directory structure
        self.bin_dir.mkdir(parents=True, exist_ok=True)
        # Copy or symlink Python executable
        self.install_python()
        # Set up site-packages
        self.install_site_packages()
        # Write pyvenv.cfg
        self.set_pyenv_cfg()

Register the entry point using a naming pattern that matches platform and Python version:

[virtualenv.create]
cpython3-posix = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix
cpython3-win = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows

Seeder plugins install initial packages into the virtual environment. Register under virtualenv.seed.

Override cannot_seed to reject target interpreters the seeder does not support. The base returns None for every interpreter; return a message instead and selection rejects the seeder before creating the environment, surfacing your message to the user. A plugin can therefore serve Python versions the bundled seeders no longer ship wheels for, such as a version past its support window.

Implement the Seeder interface:

from __future__ import annotations
from argparse import ArgumentParser
from virtualenv.app_data.base import AppData
from virtualenv.config.cli.parser import VirtualEnvOptions
from virtualenv.create.creator import Creator
from virtualenv.discovery.py_info import PythonInfo
from virtualenv.seed.seeder import Seeder
class CustomSeeder(Seeder):
    @classmethod
    def add_parser_arguments(
        cls, parser: ArgumentParser, interpreter: PythonInfo, app_data: AppData
    ) -> None:
        parser.add_argument("--custom-seed-opt", help="custom seeder option")
    @classmethod
    def cannot_seed(cls, interpreter: PythonInfo) -> str | None:
        # ship wheels down to Python 3.6, for example
        if interpreter.version_info[:2] >= (3, 6):
            return None
        return "custom seeder ships wheels only for Python 3.6 and later"
    def __init__(self, options: VirtualEnvOptions, enabled: bool) -> None:
        super().__init__(options, enabled)
        self.custom_opt = options.custom_seed_opt
    def run(self, creator: Creator) -> None:
        # Install packages into creator.bin_dir / creator.script("pip")
        self._install_packages(creator)
    def _install_packages(self, creator: Creator) -> None:
        # Implementation-specific logic
        ...

Register the entry point:

[virtualenv.seed]
custom = your_package.seed:CustomSeeder

Activator plugins generate shell activation scripts. Register under virtualenv.activate.

Implement the Activator interface:

from __future__ import annotations
from pathlib import Path
from virtualenv.activation.activator import Activator
from virtualenv.create.creator import Creator
class CustomShellActivator(Activator):
    def generate(self, creator: Creator) -> list[Path]:
        # Generate activation script content
        script_content = self._render_template(creator)
        # Write to activation directory
        dest = creator.bin_dir / self.script_name
        dest.write_text(script_content)
        return [dest]
    def _render_template(self, creator: Creator) -> str:
        # Return activation script content
        return f"""
        # Custom shell activation script
        export VIRTUAL_ENV="{creator.dest}"
        export PATH="{creator.bin_dir}:$PATH"
        """
    @property
    def script_name(self) -> str:
        return "activate.custom"

Register the entry point:

[virtualenv.activate]
bash = virtualenv.activation.bash:BashActivator
fish = virtualenv.activation.fish:FishActivator
custom = your_package.activation:CustomShellActivator

Use pyproject.toml to declare entry points:

[project]
name = "virtualenv-custom-plugin"
version = "1.0.0"
dependencies = ["virtualenv>=20.0.0"]
[project.entry-points."virtualenv.discovery"]
custom = "virtualenv_custom.discovery:CustomDiscovery"
[project.entry-points."virtualenv.create"]
custom-posix = "virtualenv_custom.creator:CustomCreator"
[project.entry-points."virtualenv.seed"]
custom = "virtualenv_custom.seeder:CustomSeeder"
[project.entry-points."virtualenv.activate"]
custom = "virtualenv_custom.activator:CustomActivator"
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

Install your plugin alongside virtualenv:

$ pip install virtualenv-custom-plugin

Or in development mode:

$ pip install -e /path/to/virtualenv-custom-plugin

Test your plugin by creating a virtual environment:

$ virtualenv --discovery=custom --creator=custom-posix --seeder=custom --activators=custom test-env

This page documents the interfaces that plugins must implement.

Discovery plugins locate Python interpreters for creating virtual environments.

Discovery plugins return a PythonInfo object describing the located interpreter.

Contains information for a Python interpreter.
Return the relative installation path for a given installation scheme key.
key (str https://docs.python.org/3/builtins/stdtypes.html#str) -- sysconfig installation scheme key (e.g. "scripts", "purelib").
str https://docs.python.org/3/builtins/stdtypes.html#str
The full version as major.minor.micro string (e.g. 3.13.2).
The python executable name as pythonX.Y (e.g. python3.13).
True if this interpreter runs inside an old-style virtualenv (has real_prefix).
True if this interpreter runs inside a PEP 405 venv (has base_prefix).
Return the sysconfig install path for a scheme key, optionally substituting config variables.
The path to the system include directory for C headers.
The prefix of the system Python this interpreter is based on.
The exec prefix of the system Python this interpreter is based on.
The executable of the system Python this interpreter is based on.

system_executable stays None until resolve_to_system() walks the prefix chain, so reading it forces every caller to narrow a value discovery has already settled. The fallback here matches what resolution itself writes when a prefix links back to its own interpreter.

A specification string identifying this interpreter (e.g. CPython3.13.2-64-arm64).
Clear all cached interpreter information from cache.
cache (PyInfoCache) -- the cache store to clear.
None https://docs.python.org/3/builtins/constants.html#None
Check if a given specification can be satisfied by this python interpreter instance.
bool https://docs.python.org/3/builtins/functions.html#bool
Locate the current host interpreter information.
cache (PyInfoCache | None https://docs.python.org/3/builtins/constants.html#None) -- interpreter metadata cache; when None results are not cached.
PythonInfo
Locate the current system interpreter information, resolving through any virtualenv layers.
cache (PyInfoCache | None https://docs.python.org/3/builtins/constants.html#None) -- interpreter metadata cache; when None results are not cached.
PythonInfo
Serialize this interpreter information to a JSON string.
Get the python information for a given executable path.
PythonInfo | None https://docs.python.org/3/builtins/constants.html#None
Deserialize interpreter information from a JSON string.
payload (str https://docs.python.org/3/builtins/stdtypes.html#str) -- JSON produced by to_json().
PythonInfo
Walk virtualenv/venv prefix chains to find the underlying system interpreter.
PythonInfo
Discover a matching Python executable under a given prefix directory.

The application data interface used by plugins for caching.

Abstract storage interface for the virtualenv application.
Called when the user passes in the reset app data.
Return a content store for cached interpreter information at the given path.
path (Path https://docs.python.org/3/library/pathlib.html#pathlib.Path) -- the interpreter executable path
ContentStore
a content store for the cached data
True if this app data store supports updating cached content.
Return a content store for the embed update log of a distribution.
ContentStore
a content store for the update log
True if this app data store is transient and does not persist across runs.
Return the path to a cached wheel image.
Path https://docs.python.org/3/library/pathlib.html#pathlib.Path
the path to the cached wheel

Creator plugins build the virtual environment directory structure and install the Python interpreter.

A class that given a python Interpreter creates a virtual environment.
  • options (VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>)
  • interpreter (PythonInfo)

Construct a new virtual environment creator.

  • options (VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>) -- the CLI option as parsed from add_parser_arguments()
  • interpreter (PythonInfo) -- the interpreter to create virtual environment from
Determine if we can create a virtual environment.
interpreter (PythonInfo) -- the interpreter in question
CreatorMeta | bool https://docs.python.org/3/builtins/functions.html#bool | None https://docs.python.org/3/builtins/constants.html#None
None if we can't create, any other object otherwise that will be forwarded to add_parser_arguments()
Add CLI arguments for the creator.
None https://docs.python.org/3/builtins/constants.html#None
Generate a file indicating that this is not meant to be backed up.
Generate ignore instructions for version control systems.
Point the .venv redirect file of the parent folder at the environment, per PEP-832 (provisional).

Seeder plugins install initial packages (like pip, setuptools, wheel) into the virtual environment.

A seeder will install some seed packages into a virtual environment.

Create.

Explain why this seeder cannot install seed packages for the given interpreter.
interpreter (PythonInfo) -- the interpreter the environment is based on
str https://docs.python.org/3/builtins/stdtypes.html#str | None https://docs.python.org/3/builtins/constants.html#None
None when the seeder supports the interpreter, otherwise a message describing why it cannot; selection rejects a seeder that returns a message and surfaces it to the user
Add CLI arguments for this seed mechanisms.
None https://docs.python.org/3/builtins/constants.html#None
Perform the seed operation.
creator (Creator) -- the creator (based of virtualenv.create.creator.Creator) we used to create this virtual environment
None https://docs.python.org/3/builtins/constants.html#None

Activator plugins generate shell-specific activation scripts.

Generates activate script for the virtual environment.
options (VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>)

Create a new activator generator.

options (VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>) -- the parsed options as defined within add_parser_arguments()
Check if the activation script is supported in the given interpreter.
interpreter (PythonInfo) -- the interpreter we need to support
bool https://docs.python.org/3/builtins/functions.html#bool
True if supported, False otherwise
Add CLI arguments for this activation script.
None https://docs.python.org/3/builtins/constants.html#None
Generate activate script for the given creator.
creator (Creator) -- the creator (based of virtualenv.create.creator.Creator) we used to create this virtual environment
list https://docs.python.org/3/builtins/stdtypes.html#list[Path https://docs.python.org/3/library/pathlib.html#pathlib.Path]

This page explains how virtualenv's plugin system works internally.

virtualenv uses Python entry points (setuptools / importlib.metadata) to discover plugins. Each plugin registers under one of four entry point groups:

  • virtualenv.discovery
  • virtualenv.create
  • virtualenv.seed
  • virtualenv.activate

At startup, virtualenv loads all registered entry points from these groups and makes them available as CLI options. Built-in implementations are registered in virtualenv's own pyproject.toml, while third-party plugins register their entry points in their own package metadata.

When a package with virtualenv plugins is installed in the same environment as virtualenv, the plugins become immediately available without additional configuration.

The following diagram shows how plugins are discovered and executed: [graph]

The lifecycle follows these stages:

1.
virtualenv starts and discovers all entry points from the four plugin groups
2.
The CLI parser is built dynamically, incorporating options from all discovered plugins
3.
User arguments are parsed to select which discovery, creator, seeder, and activator plugins to use
4.
Selected plugins execute in sequence: discover → create → seed → activate
5.
Each stage passes its output to the next stage

Each extension point follows a consistent pattern:

Each extension point defines a base abstract class (Discover, Creator, Seeder, Activator) that specifies the interface plugins must implement.
virtualenv includes built-in implementations registered as entry points in its own pyproject.toml. For example, the built-in CPython creator is registered as cpython3-posix.
External packages implement the base interface and register their own entry points under the same group. When installed, they appear alongside built-in options.
Command-line flags (--discovery, --creator, --seeder, --activators) allow users to select which implementation to use. Multiple activators can be selected simultaneously.
Each plugin can contribute CLI arguments through the add_parser_arguments classmethod. These arguments appear in virtualenv --help and are available when the plugin is selected.

Plugins execute in a pipeline where each stage depends on the previous one:

Discovery → Creator
The discovery plugin produces a PythonInfo object describing the source Python interpreter. This object contains metadata about the Python version, platform, paths, and capabilities. The creator plugin receives this PythonInfo and uses it to determine how to build the virtual environment structure.
The creator plugin produces a Creator object representing the newly created virtual environment. This includes paths to the environment's bin directory, site-packages, and Python executable. The seeder plugin uses these paths to install packages.
After seeding completes, activator plugins use the Creator object to generate shell activation scripts. These scripts reference the environment's bin directory and other paths to configure the shell environment.

This pipeline ensures that each plugin has the information it needs from previous stages. The PythonInfo flows from discovery to creator, and the Creator object flows from creator to both seeder and activators.

Plugins within the same extension point do not interact with each other. Only one discovery and one creator plugin can run per invocation, though multiple activators can run simultaneously. This isolation keeps plugins simple and focused on their specific task.

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 https://github.com/pypa/virtualenv/issues/new?title=Trouble+with+development+environment about it on the issue tracker.

virtualenv is a command line application written in Python. To work on it, you'll need:

•
git clone https://github.com/pypa/virtualenv
cd virtualenv

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

virtualenv's tests are written using the pytest https://pypi.org/project/pytest test framework. tox https://pypi.org/project/tox is used to automate the setup and execution of virtualenv's tests.

To run tests locally execute:

tox -e py

This will run the test suite for the same Python version as under which tox is installed. Alternatively you can specify a specific version of python by using the pyNN format, such as: py314, pypy3, etc.

tox has been configured to forward any additional arguments it is given to pytest. This enables the use of pytest's rich CLI https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests. 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.14:

tox -e tasks

virtualenv uses pre-commit https://pypi.org/project/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.

virtualenv ships a PEP 561 https://peps.python.org/pep-0561/ 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 https://docs.astral.sh/ty/ (Astral's Rust-based type checker) to validate annotations against Python 3.14. A second environment checks compatibility with the minimum supported version:

tox -e type-3.9

Both environments validate that annotations are consistent and correct.

  • Use from __future__ import annotations at the top of every module (enforced by ruff's required-imports setting).
  • Place imports that are only needed for type checking inside an if TYPE_CHECKING: block to avoid runtime overhead.
  • Ruff's ANN rules are enabled. ANN401 (typing.Any) is suppressed on a case-by-case basis with inline # noqa: ANN401 comments where Any is genuinely required (e.g. serialization, dynamic dispatch).
  • Prefer concrete types over Any. Use Union / | 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.

virtualenv's documentation is built using Sphinx https://pypi.org/project/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.

Two logo files live in docs/_static and serve different roles, so both are kept on purpose:

  • virtualenv.png is the official logo — the snakes wrapped around a terminal, with the wordmark. It is the site logo (light_logo/dark_logo in docs/conf.py) and the one to use wherever a virtualenv logo is needed.
  • virtualenv.svg is 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.

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

A full release publishes to PyPI https://pypi.org/project/virtualenv/, creates a GitHub Release https://github.com/pypa/virtualenv/releases with the zipapp and SBOMs attached, and updates get-virtualenv https://github.com/pypa/get-virtualenv so that https://bootstrap.pypa.io/virtualenv.pyz serves the new version.

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 https://github.com/pypa/virtualenv/actions/workflows/release.yaml which builds the sdist, wheel, and zipapp, publishes to PyPI via trusted publisher, creates a GitHub Release https://github.com/pypa/virtualenv/releases with the zipapp and SBOMs attached, and updates get-virtualenv https://github.com/pypa/get-virtualenv. A failed publish needs the recovery procedure below.

Via GitHub Actions (recommended)

1.
Go to the Pre-release workflow https://github.com/pypa/virtualenv/actions/workflows/pre-release.yaml on GitHub.
2.
Click Run workflow and select the bump type (auto, major, minor, or patch).

Locally

tox r -e release

Pass --version <bump> to override the default auto behavior (e.g. --version minor).

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 https://pypi.org/help/#file-name-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.

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 <#compatibility-requirements> 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 https://www.kessler.de/prd/smartbear/BestPracticesForPeerCodeReview.pdf 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.

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 <#compatibility-requirements> 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 https://github.com/pypa/virtualenv/blob/main/.github/THREAT_MODEL.md#update-triggers 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 https://github.com/pypa/virtualenv/blob/main/.github/THREAT_MODEL.md#library-and-supply-chain-threats records the risk of a single reviewer.

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.

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.toml allows.
  • The daily upgrade.yaml workflow runs tox r -e upgrade, which downloads new pip and setuptools wheels from PyPI into src/virtualenv/seed/wheels/embed and records their SHA-256 in BUNDLE_SHA256. The same workflow recompiles pylock.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.yaml freezes to commit SHAs. upgrade.yaml bumps 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.yaml fails a pull request that adds a dependency with a known advisory of any severity, in runtime or development scope. The main ruleset requires that check.
  • A shipped package whose license falls outside ALLOWED_LICENSES fails the SBOM check in tox r -e readme and tox r -e zipapp, which the required ✅ all checks pass check 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 <#embedded-wheel-advisories> lists its advisories and the way around them.

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_LICENSES in tasks/license_policy.py. That covers virtualenv itself, every distribution the zipapp bundles, and the embedded pip and setuptools wheels under src/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 covers filelock 3.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.py enforces the policy on the SBOMs of the built wheel and zipapp, in tox r -e readme and tox r -e zipapp. An SPDX expression passes when each OR offers an allowed license and every AND operand is allowed; a WITH exception leaves the verdict to the license it modifies. A package that declares its license by name alone fails the check until _CLASSIFIER_LICENSES in hatch_build.py maps the name to an SPDX id.
  • .github/workflows/dependency-review.yaml fails 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.py writes a CycloneDX https://cyclonedx.org/ SBOM into every wheel at .dist-info/sboms/virtualenv.cdx.json. It reads the BUNDLE_SUPPORT/BUNDLE_SHA256 tables that back the embedded wheels above, so a wheel bump updates it with no extra step. tasks/cyclonedx_to_spdx.py renders that document as SPDX 2.3 with the standard library alone. tox r -e readme builds a wheel and validates its SBOM and the SPDX rendering; the release workflow runs tox r -e spdx to write virtualenv.spdx.json from virtualenv.cdx.json and check it with pyspdxtools. Both envs run on Python 3.14 because spdx-tools fails to import on 3.15.
  • tasks/zipapp_sbom.py describes a built virtualenv.pyz from 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 in hatch_build.py, writes virtualenv.pyz.cdx.json next to the zipapp and adds the same file to the archive root. tox r -e zipapp builds the zipapp, runs the script, and has tasks/validate_sbom.py check 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.

All pull requests and merges to 'main' branch are tested using GitHub Actions https://docs.github.com/en/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.

Static analysis runs on every pull request and every push to main:

  • pre-commit.ci runs ruff with every rule selected, the flake8-bandit security rules included, and zizmor https://docs.zizmor.sh on the workflows. The type and type-3.9 tox environments run ty.
  • codeql.yaml runs CodeQL with the security-extended suite and the project queries under .github/codeql/queries on 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 main ruleset blocks a merge while CodeQL reports an alert of severity error, or a security alert of severity high or critical.
  • 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 https://github.com/pypa/virtualenv/blob/main/.github/THREAT_MODEL.md lists. An inline suppression names the rule it silences, and Running linters says when a # noqa fits.

The changelog.rst file is managed using towncrier https://pypi.org/project/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 https://pypi.org/project/towncrier will deduplicate them.

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.

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.

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

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.

The project stores two secrets, the client ID and the private key of the virtualenv-release https://github.com/apps/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 https://docs.pypi.org/trusted-publishers/, 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 release environment, 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_KEY and 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 https://github.com/pypa/virtualenv/blob/main/.github/INCIDENT_RESPONSE.md#response.

•
Paul Moore https://github.com/pfmoore, who steps in if both current maintainers are away

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.

  • Backport fixes to older release lines; see supported versions https://github.com/pypa/virtualenv/blob/main/.github/SECURITY.md#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 <#compatibility-requirements> has dropped.
  • Write files outside the environment it creates, such as a project-level .gitignore (#3237 https://github.com/pypa/virtualenv/issues/3237).
  • Install, resolve or lock project dependencies. virtualenv creates and seeds the environment; pip, uv and similar tools manage what goes into it.

No significant changes.

Bugfixes - 21.14.5

•
Fall back to the bundled seed wheel and remove the embed update log when the log in the app data folder holds JSON of the wrong shape, instead of failing to create the environment - by @pasmud https://github.com/pasmud. (#3376 https://github.com/pypa/virtualenv/issues/3376)

Bugfixes - 21.14.4

•
Fix activate running commands from a virtual environment name or --prompt that holds $(...), backticks or ${...} when zsh has PROMPT_SUBST set, and show a % in the name as typed under zsh (GHSA-5vjq-rrrf-7h2q https://github.com/pypa/virtualenv/security/advisories/GHSA-5vjq-rrrf-7h2q); reported by @kemrec https://github.com/kemrec. (#3373 https://github.com/pypa/virtualenv/issues/3373)

Bugfixes - 21.14.3

•
Honor ~= and every clause of a seed wheel's Requires-Python when picking a wheel for the target Python, and skip a wheel whose Requires-Python is not a valid specifier instead of failing - by @pasmud https://github.com/pasmud. (#3369 https://github.com/pypa/virtualenv/issues/3369)

Bugfixes - 21.14.2

No significant changes.

Features - 21.14.0

Bugfixes - 21.14.0

Improved Documentation - 21.14.0

Features - 21.13.0

•
Add Changelog and Funding links to the PyPI project metadata, and replace the 2020-202x placeholder in LICENSE with 2020-present so the copyright field of the wheel SBOM reads as a real range - by @gaborbernat https://github.com/gaborbernat. (#3334 https://github.com/pypa/virtualenv/issues/3334)

Bugfixes - 21.13.0

Improved Documentation - 21.13.0

Bugfixes - 21.12.1

•
Limit the PEP 832 https://peps.python.org/pep-0832/ .venv redirect to folders holding a pyproject.toml and no .venv yet, so virtualenv foo in a scratch folder, and tools such as tox or nox building environments through virtualenv, no longer claim a folder's default environment - by @gaborbernat https://github.com/gaborbernat.
  • --venv-redirect writes the redirect in any folder and replaces an earlier virtualenv redirect.
  • A flag on the command line overrides the environment variable and the config file in either direction. (#3316 https://github.com/pypa/virtualenv/issues/3316)

Features - 21.12.0

  • virtualenv leaves a .venv folder alone, and a redirect pointing at an environment it did not create.
  • Pass --no-venv-redirect to opt out.
  • The feature is provisional while PEP 832 is a draft: a minor or patch release may change it in backward incompatible ways to follow the PEP. (#3204 https://github.com/pypa/virtualenv/issues/3204)

Bugfixes - 21.11.1

•
Include the pre-commit configuration and the zipapp lock file in the source distribution, so downstream packagers can run the test suite from it. (#3314 https://github.com/pypa/virtualenv/issues/3314)

Features - 21.11.0

  • Attach the CycloneDX SBOM and an SPDX 2.3 rendering of it (virtualenv.cdx.json, virtualenv.spdx.json) to each GitHub release, and attest the SPDX document against the sdist and wheel. (#3299 https://github.com/pypa/virtualenv/issues/3299)
  • Describe the zipapp in its own CycloneDX SBOM, which lists virtualenv, the embedded pip and setuptools wheels, and each bundled dependency with the Python versions that load it, down to a SHA-256 per file. The SBOM sits at the root of virtualenv.pyz, ships as the virtualenv.pyz.cdx.json release asset, and GitHub attests it against the zipapp. (#3310 https://github.com/pypa/virtualenv/issues/3310)

Bugfixes - 21.11.0

  • Refuse a seed wheel downloaded from the default index when virtualenv cannot get its sha256 from PyPI after three retries, instead of seeding it unverified. Seeding falls back to the bundled wheel unless a specific version was requested. (#3302 https://github.com/pypa/virtualenv/issues/3302)
  • Build the zipapp from the dependency wheels that pylock.zipapp.toml pins by URL and sha256, so a release bundles only files matching the committed hashes. (#3306 https://github.com/pypa/virtualenv/issues/3306)
  • Leave the build machine out of the wheel and zipapp SBOMs, so the wheel rebuilds byte for byte on any operating system and architecture given the same source, SOURCE_DATE_EPOCH, Python patch version and build backend versions. Zipapp entries now carry the SOURCE_DATE_EPOCH timestamp and fixed permissions instead of the build time. (#3311 https://github.com/pypa/virtualenv/issues/3311)

Improved Documentation - 21.11.0

Features - 21.10.0

Bugfixes - 21.10.0

•
Skip Windows batch activation scripts when environment or Tcl/Tk paths contain ^ or !, since quoting changes those paths. Other activation scripts remain available. (#3280 https://github.com/pypa/virtualenv/issues/3280)

Bugfixes - 21.9.1

•
Correct SBOM CSV parsing and unresolved dependency relationships, and omit CI run identifiers from reproducible builds. (#3278 https://github.com/pypa/virtualenv/issues/3278)

Features - 21.9.0

•
The embedded SBOM now describes the root component's license, copyright, maintainers and project links, each bundled wheel from its own metadata, the declared runtime dependencies, and the full build environment (interpreter, OS and every distribution in the isolated build environment with the dependency graph between them), plus the GitHub Actions run when built there. (#3270 https://github.com/pypa/virtualenv/issues/3270)

Bugfixes - 21.9.0

  • Add metadata.timestamp (derived from SOURCE_DATE_EPOCH for reproducibility) and metadata.tools naming the generator to the embedded SBOM, and validate both fields in the packaging checks that already run on every pull request. (#3269 https://github.com/pypa/virtualenv/issues/3269)
  • Fix the embedded SBOM not conforming to the CycloneDX 1.6 schema (an invalid vendor key on the generator tool entry) and validate it against the schema in CI. (#3270 https://github.com/pypa/virtualenv/issues/3270)

Bugfixes - 21.8.1

•
Make the embedded SBOM's serialNumber a deterministic UUID derived from the package name, version and bundled wheel hashes, so the same source tree produces a byte-identical SBOM, and validate the SBOM's structure as part of the packaging checks that already run on every pull request. (#3268 https://github.com/pypa/virtualenv/issues/3268)

Features - 21.8.0

•
Every published wheel now carries a CycloneDX https://cyclonedx.org/ SBOM at .dist-info/sboms/virtualenv.cdx.json (PEP 770 https://peps.python.org/pep-0770/), declaring the bundled pip and setuptools wheels that generic SBOM tools cannot see on their own. GitHub attests it against the released sdist and wheel. (#3265 https://github.com/pypa/virtualenv/issues/3265)

Bugfixes - 21.7.16

•
Fix activate.csh rendering the venv's prompt text wrong when it contains ! (csh and tcsh both expand it) or % (tcsh only, since plain csh has no % prompt escape at all). (#3263 https://github.com/pypa/virtualenv/issues/3263)

Bugfixes - 21.7.15

Bugfixes - 21.7.14

•
Fix activate.csh failing with Event not found when the virtual environment path contains !. (#3256 https://github.com/pypa/virtualenv/issues/3256)

Bugfixes - 21.7.13

•
Fix activate and activate.fish running commands embedded in the virtual environment path or in the interpreter's Tcl/Tk library paths. (#3252 https://github.com/pypa/virtualenv/issues/3252)

Bugfixes - 21.7.12

Bugfixes - 21.7.11

  • Running activate.bat again before deactivate no longer makes deactivate leave the environment's PKG_CONFIG_PATH, TCL_LIBRARY and TK_LIBRARY behind, or lose values the user had set before the first activation - by @darrenhuai https://github.com/darrenhuai. (#3245 https://github.com/pypa/virtualenv/issues/3245)
  • Write pyvenv.cfg values on a single line, so a prompt carrying a line boundary can no longer inject configuration. --prompt, the VIRTUALENV_PROMPT environment variable and the config file all set the prompt, and pyvenv.cfg has no escape syntax, so a newline, a carriage return, or any other boundary str.splitlines recognizes, such as U+2028, started a new configuration line. Reading the file back picked up those lines as keys, and since the last value for a key wins, they replaced anything written earlier, including home. (#3247 https://github.com/pypa/virtualenv/issues/3247)

Improved Documentation - 21.7.11

•
Document the policy for AI-assisted contributions and the licensing rules for dependencies. (#3239 https://github.com/pypa/virtualenv/issues/3239)

Misc - 21.7.11

Bugfixes - 21.7.10

  • Skip blank lines, # comments and lines without = in pyvenv.cfg instead of raising ValueError - by @r3wretrhy https://github.com/r3wretrhy. (#3232 https://github.com/pypa/virtualenv/issues/3232)
  • deactivate in bash, fish and PowerShell unsets PKG_CONFIG_PATH when activation found it unset, instead of keeping the environment's lib/pkgconfig. csh activation no longer drops a PKG_CONFIG_PATH the user had set. Activation in batch, fish, nushell and PowerShell no longer adds a trailing separator when PKG_CONFIG_PATH is unset, and PowerShell and nushell build the value with the host's path separators - by @darrenhuai https://github.com/darrenhuai. (#3233 https://github.com/pypa/virtualenv/issues/3233)
  • Activation in bash, csh, fish and PowerShell keeps the user's TCL_LIBRARY and TK_LIBRARY, and deactivate restores them. csh and PowerShell removed both variables on every activation, fish did so when the interpreter has tcl, and bash kept the environment's value after deactivate when the variable was unset before - by @darrenhuai https://github.com/darrenhuai. (#3234 https://github.com/pypa/virtualenv/issues/3234)

Bugfixes - 21.7.9

Bugfixes - 21.7.8

•
A missing source needed by both install modes now disables the builtin creator through meta.error. The check compared RefWhen values against RefMust members, so creation went on to fail with a FileNotFoundError or a dangling symlink - by @darrenhuai https://github.com/darrenhuai. (#3227 https://github.com/pypa/virtualenv/issues/3227)

Bugfixes - 21.7.7

•
Bump the python-discovery minimum to >=1.6 for PythonInfo.system_exe, which reports the system interpreter without the nullable typing of system_executable - by @gaborbernat https://github.com/gaborbernat. (#3224 https://github.com/pypa/virtualenv/issues/3224)

Improved Documentation - 21.7.7

•
Document the names the interpreter answers to inside a created environment. A new Environment layout <> page lists them per platform, the tutorial and the usage guide point at it, and the explanation of creators covers why an environment carries several names and why Windows copies a redirector - by @gaborbernat https://github.com/gaborbernat. (#3225 https://github.com/pypa/virtualenv/issues/3225)

Bugfixes - 21.7.6

•
On Windows, virtualenv no longer copies the CPython 3.13+ venvlauncher.exe shim into Scripts under the shim's own name, and a host such as python_d.exe gets its alias back. The alias set took its names from the shim that stands in for the interpreter, so every environment gained a stray launcher copy and lost the interpreter's own file name - by @darrenhuai https://github.com/darrenhuai. (#3223 https://github.com/pypa/virtualenv/issues/3223)

Bugfixes - 21.7.5

  • Fix the type check against python-discovery 1.5.2, whose annotations allow a None prefix and integer sysconfig_vars values: config var substitution now skips a missing prefix and locating the shared libpython requires string INSTSONAME/LIBDIR values. (#3211 https://github.com/pypa/virtualenv/issues/3211)
  • ExePathRef.can_run now checks the group and other execute bits instead of only the owner one, and returns False rather than None when a file carries none of the three - by @darrenhuai https://github.com/darrenhuai. (#3217 https://github.com/pypa/virtualenv/issues/3217)
  • safe_delete no longer passes ignore_errors=True to shutil.rmtree, which replaced its own chmod-and-retry handler with a no-op and swallowed every failure. Read-only files - every file in a wheel image, which set_tree marks - survived, so --reset-app-data and --clear kept trees they reported deleting. The handler now retries only the deletion itself, keeps the other mode bits, and raises the original error for anything it cannot clear - by @darrenhuai https://github.com/darrenhuai. (#3222 https://github.com/pypa/virtualenv/issues/3222)

Bugfixes - 21.7.4

•
Upgrade embedded wheels:
•
setuptools to 84.0.0 from 83.0.0 (#3208 https://github.com/pypa/virtualenv/issues/3208)

Bugfixes - 21.7.3

•
ListType.split_values now returns a list you can iterate more than once, and accepts bytes input instead of raising TypeError - by @darrenhuai https://github.com/darrenhuai. (#3207 https://github.com/pypa/virtualenv/issues/3207)

Bugfixes - 21.7.2

•
Upgrade embedded wheels:
•
pip to 26.2.1 from 26.2 (#3206 https://github.com/pypa/virtualenv/issues/3206)

Bugfixes - 21.7.1

•
Upgrade embedded wheels:
•
pip to 26.2 from 26.1.2 (#u https://github.com/pypa/virtualenv/issues/u)

Improved Documentation - 21.7.1

•
Document that virtualenv.png is the official logo and virtualenv.svg is a simplified favicon-only mark - by @gaborbernat https://github.com/gaborbernat. (#3199 https://github.com/pypa/virtualenv/issues/3199)

Features - 21.7.0

•
Declare support for Python 3.15: CI now tests against the CPython 3.15 beta, including the free-threaded 3.15t build, and the Programming Language :: Python :: 3.15 classifier is advertised. (#3192 https://github.com/pypa/virtualenv/issues/3192)

Bugfixes - 21.6.1

•
Harden the fish activator prompt against user functions that shadow builtins. Routing functions, printf, string, echo and source/. through builtin stops a shadowing function (such as a dot-style directory navigator that redefines .) from hijacking the prompt and dropping the previous command's exit status, matching the CPython fix in gh-140006 https://github.com/python/cpython/issues/140006. (#3185 https://github.com/pypa/virtualenv/issues/3185)

Features - 21.6.0

•
Stop installing the _virtualenv.{py,pth} distutils import hook for Python 3.10 and later, where pip, setuptools and CPython already ignore the install config keys it guards against; this removes the hook's startup import cost. The hook is still installed for Python 3.9 - #3181 https://github.com/pypa/virtualenv/issues/3181. (#3181 https://github.com/pypa/virtualenv/issues/3181)

Bugfixes - 21.5.2

•
Upgrade embedded wheels:
•
setuptools to 83.0.0 (#3180 https://github.com/pypa/virtualenv/issues/3180)

Bugfixes - 21.5.1

•
Refuse to create environments whose Python the bundled wheels no longer cover (currently below 3.9). virtualenv used to substitute the newest bundled pip, which cannot run on such a target, leaving a broken environment; seeder selection now rejects it up front with a clear error. --no-seed and third-party seeders that ship compatible wheels still work - by @gaborbernat https://github.com/gaborbernat. (#3171 https://github.com/pypa/virtualenv/issues/3171)

Features - 21.5.0

•
Drop support for Python 3.8; virtualenv now requires Python 3.9 or later to run and to create environments. Remove the embedded wheel seed package, which virtualenv bundled only for Python 3.8. The --wheel and --no-wheel options stay as no-ops, but now warn that virtualenv will remove them in a release after 2026-12 - by @gaborbernat https://github.com/gaborbernat. (#3170 https://github.com/pypa/virtualenv/issues/3170)

Bugfixes - 21.5.0

•
Upgrade embedded wheels:

Removed wheel of 0.47.0 (#u https://github.com/pypa/virtualenv/issues/u)

Bugfixes - 21.4.3

•
Upgrade embedded wheels:
•
pip to 26.1.2 from 26.1.1 (#u https://github.com/pypa/virtualenv/issues/u)

Bugfixes - 21.4.2

•
Stop deactivate in the bash/zsh activation script from aborting under set -e when hash -r fails (for example with shell hashing disabled) by appending || true, matching CPython venv (gh-149701) and the existing non-deactivate call - by @gaborbernat https://github.com/gaborbernat. (#3152 https://github.com/pypa/virtualenv/issues/3152)

Bugfixes - 21.4.1

•
Fix Windows debug build venvlauncher_d.exe substitution never triggering because executables() compared the source executable name instead of the target name, and fix AttributeError on debug_build attribute for interpreter info objects missing the field - by @gaborbernat https://github.com/gaborbernat. (#3151 https://github.com/pypa/virtualenv/issues/3151)

Features - 21.4.0

Bugfixes - 21.3.3

•
recognize GraalPy interpreters using the normalized GraalPy name - by @timfel https://github.com/timfel. (#3144 https://github.com/pypa/virtualenv/issues/3144)

No significant changes.

Bugfixes - 21.3.1

•
Upgrade embedded wheels:
•
pip to 26.1.1 from 26.1 (#3138 https://github.com/pypa/virtualenv/issues/3138)

Features - 21.3.0

•
Re-introduce xonsh shell activator (activate.xsh) previously removed in 20.7.0, and make the plugin loader prefer virtualenv's built-in entry points so a third-party package cannot override them by registering a duplicate name. (#3003 https://github.com/pypa/virtualenv/issues/3003)

Bugfixes - 21.3.0

•
Upgrade embedded wheels:

Bugfixes - 21.2.4

  • Security hardening: validate each entry of a seed wheel archive before extracting it so a tampered wheel cannot escape the app-data image directory via an absolute path or .. traversal. (#3118 https://github.com/pypa/virtualenv/issues/3118)
  • Security hardening: verify the SHA-256 of every bundled seed wheel when it is loaded so a corrupted or tampered file on disk fails loud instead of being handed to pip. The hash table is generated alongside BUNDLE_SUPPORT by tasks/upgrade_wheels.py. (#3119 https://github.com/pypa/virtualenv/issues/3119)
  • Security hardening: validate the distribution name and version specifier passed to pip download when acquiring a seed wheel so extras, pip flags, or shell metacharacters cannot be smuggled into the subprocess command line. (#3120 https://github.com/pypa/virtualenv/issues/3120)
  • Security hardening: replace the string-prefix containment check in virtualenv.util.zipapp with Path.relative_to so the zipapp extraction helpers refuse any path that does not resolve under the archive root. (#3121 https://github.com/pypa/virtualenv/issues/3121)
  • Security hardening: do not silently fall back to an unverified HTTPS context when the periodic update request to PyPI fails TLS verification. The returned metadata drives which wheel version virtualenv considers "up to date", so accepting an unverified response lets a network-level attacker suppress security updates. Set VIRTUALENV_PERIODIC_UPDATE_INSECURE=1 to restore the previous behavior on hosts with broken trust stores. (#3122 https://github.com/pypa/virtualenv/issues/3122)

No significant changes.

Bugfixes - 21.2.2

•
Bump python-discovery minimum to >=1.2.2 to include normalize_isa support - by @rahuldevikar https://github.com/rahuldevikar. (#3117 https://github.com/pypa/virtualenv/issues/3117)

Bugfixes - 21.2.1

•
Upgrade embedded wheels:
•
setuptools to 82.0.1 from 82.0.0 (#3093 https://github.com/pypa/virtualenv/issues/3093)
•
Use terminal width for help formatting instead of hardcoded 240. (#3110 https://github.com/pypa/virtualenv/issues/3110)

Features - 21.2.0

•
Update embed wheel generator (tasks/upgrade_wheels.py) to include type annotations in generated output - by @rahuldevikar https://github.com/rahuldevikar. (#3075 https://github.com/pypa/virtualenv/issues/3075)

Bugfixes - 21.2.0

Features - 21.1.0

•
Add comprehensive type annotations across the entire codebase and ship a PEP 561 py.typed marker so downstream consumers and type checkers recognize virtualenv as an inline-typed package - by @rahuldevikar https://github.com/rahuldevikar. (#3075 https://github.com/pypa/virtualenv/issues/3075)

Deprecations and Removals - 21.0.0

•
The Python discovery logic has been extracted into a standalone python-discovery package on PyPI (documentation https://python-discovery.readthedocs.io/) and is now consumed as a dependency. If you previously imported discovery internals directly (e.g. from virtualenv.discovery.py_info import PythonInfo), switch to from python_discovery import PythonInfo. Backward-compatibility re-export shims are provided at virtualenv.discovery.py_info, virtualenv.discovery.py_spec, and virtualenv.discovery.cached_py_info, however these are considered unsupported and may be removed in a future release - by @gaborbernat https://github.com/gaborbernat. (#3070 https://github.com/pypa/virtualenv/issues/3070)

Features - 20.39.1

•
Add support for creating virtual environments with RustPython - by @elmjag https://github.com/elmjag. (#3010 https://github.com/pypa/virtualenv/issues/3010)

Features - 20.39.0

  • Automatically resolve version manager shims (pyenv, mise, asdf) to the real Python binary during discovery, preventing incorrect interpreter selection when shims are on PATH - by @gaborbernat https://github.com/gaborbernat. (#3049 https://github.com/pypa/virtualenv/issues/3049)
  • Add architecture (ISA) awareness to Python discovery — users can now specify a CPU architecture suffix in the --python spec string (e.g. cpython3.12-64-arm64) to distinguish between interpreters that share the same version and bitness but target different architectures. Uses sysconfig.get_platform() as the data source, with cross-platform normalization (amd64 ↔ x86_64, aarch64 ↔ arm64). Omitting the suffix preserves existing behavior - by @rahuldevikar https://github.com/rahuldevikar. (#3059 https://github.com/pypa/virtualenv/issues/3059)

Features - 20.38.0

Bugfixes - 20.38.0

Bugfixes - 20.36.1

•
Fix TOCTOU vulnerabilities in app_data and lock directory creation that could be exploited via symlink attacks - reported by @tsigouris007 https://github.com/tsigouris007, fixed by @gaborbernat https://github.com/gaborbernat. (#3013 https://github.com/pypa/virtualenv/issues/3013)

Features - 20.36.0

•
Add support for PEP 440 version specifiers in the --python flag. Users can now specify Python versions using operators like >=, <=, ~=, etc. For example: virtualenv --python=">=3.12" myenv . (:issue:`2994)

Bugfixes - 20.35.4

•
pip to 25.3 from 25.2 (#2989 https://github.com/pypa/virtualenv/issues/2989)

Bugfixes - 20.35.3

•
Accept RuntimeError in test_too_many_open_files, by @esafak https://github.com/esafak (#2935 https://github.com/pypa/virtualenv/issues/2935)

Bugfixes - 20.35.2

•
Revert out changes related to the extraction of the discovery module - by @gaborbernat https://github.com/gaborbernat. (#2978 https://github.com/pypa/virtualenv/issues/2978)

Bugfixes - 20.35.1

Features - 20.35.0

Bugfixes - 20.35.0

Features - 20.34.0

Bugfixes - 20.34.0

Bugfixes - 20.33.1

Features - 20.33.0

•
Added support for Tcl and Tkinter. You're welcome. Contributed by @esafak https://github.com/esafak. (#425 https://github.com/pypa/virtualenv/issues/425)

Bugfixes - 20.33.0

Features - 20.32.0

Bugfixes - 20.32.0

No significant changes.

Bugfixes - 20.31.1

•
Upgrade embedded wheels:

Features - 20.31.0

•
No longer bundle wheel wheels (except on Python 3.8), setuptools includes native bdist_wheel support. Update pip to 25.1. (#2868 https://github.com/pypa/virtualenv/issues/2868)

Bugfixes - 20.31.0

Features - 20.30.0

Bugfixes - 20.30.0

•
Upgrade embedded wheels:
•
setuptools to 78.1.0 from 75.3.2 (#2863 https://github.com/pypa/virtualenv/issues/2863)

Bugfixes - 20.29.3

•
Ignore unreadable directories in PATH. (#2794 https://github.com/pypa/virtualenv/issues/2794)

Bugfixes - 20.29.2

Bugfixes - 20.29.1

•
Fix PyInfo cache incompatibility warnings - by @robsdedude https://github.com/robsdedude. (#2827 https://github.com/pypa/virtualenv/issues/2827)

Features - 20.29.0

•
Add support for selecting free-threaded Python interpreters, e.g., python3.13t. (#2809 https://github.com/pypa/virtualenv/issues/2809)

Bugfixes - 20.29.0

•
Upgrade embedded wheels:
•
setuptools to 75.8.0 from 75.6.0 (#2823 https://github.com/pypa/virtualenv/issues/2823)

Bugfixes - 20.28.1

•
Skip tcsh tests on broken tcsh versions - by @gaborbernat https://github.com/gaborbernat. (#2814 https://github.com/pypa/virtualenv/issues/2814)

Features - 20.28.0

•
Write CACHEDIR.TAG file on creation - by "user:neilramsay. (#2803 https://github.com/pypa/virtualenv/issues/2803)

Bugfixes - 20.27.2

•
Upgrade embedded wheels:
•
setuptools to 75.3.0 from 75.2.0 (#2798 https://github.com/pypa/virtualenv/issues/2798)
•
Upgrade embedded wheels:

Bugfixes - 20.27.1

•
Upgrade embedded wheels:
•
pip to 24.3.1 from 24.2 (#2789 https://github.com/pypa/virtualenv/issues/2789)

Features - 20.27.0

•
Drop 3.7 support as the CI environments no longer allow it running - by @gaborbernat https://github.com/gaborbernat. (#2758 https://github.com/pypa/virtualenv/issues/2758)

Bugfixes - 20.27.0

•
Fix zipapp is broken on Windows post distlib 0.3.9 - by @gaborbernat https://github.com/gaborbernat. (#2784 https://github.com/pypa/virtualenv/issues/2784)

Bugfixes - 20.26.6

•
Properly quote string placeholders in activation script templates to mitigate potential command injection - by @y5c4l3 https://github.com/y5c4l3. (#2768 https://github.com/pypa/virtualenv/issues/2768)

Bugfixes - 20.26.5

•
Upgrade embedded wheels: setuptools to 75.1.0 from 74.1.2 - by @gaborbernat https://github.com/gaborbernat. (#2765 https://github.com/pypa/virtualenv/issues/2765)

Bugfixes - 20.26.4

Bugfixes - 20.26.3

•
Upgrade embedded wheels:

Bugfixes - 20.26.2

Bugfixes - 20.26.1

•
fix PATH-based Python discovery on Windows - by @ofek https://github.com/ofek. (#2712 https://github.com/pypa/virtualenv/issues/2712)

Bugfixes - 20.26.0

•
allow builtin discovery to discover specific interpreters (e.g. python3.12) given an unspecific spec (e.g. python3) - by @flying-sheep https://github.com/flying-sheep. (#2709 https://github.com/pypa/virtualenv/issues/2709)

Bugfixes - 20.25.3

•
Python 3.13.0a6 renamed pathmod to parser. (#2702 https://github.com/pypa/virtualenv/issues/2702)

Bugfixes - 20.25.2

•
Upgrade embedded wheels:

Bugfixes - 20.25.1

•
Upgrade embedded wheels:
•
Upgrade embedded wheels:

Misc - 20.25.1

Features - 20.25.0

•
The tests now pass on the CI with Python 3.13.0a2 - by @hroncok https://github.com/hroncok. (#2673 https://github.com/pypa/virtualenv/issues/2673)

Bugfixes - 20.25.0

•
Upgrade embedded wheels:
•
wheel to 0.41.3 from 0.41.2 (#2665 https://github.com/pypa/virtualenv/issues/2665)
•
Upgrade embedded wheels:

Bugfixes - 20.24.6

Bugfixes - 20.24.5

•
setuptools to 68.2.0 from 68.1.2 (#2642 https://github.com/pypa/virtualenv/issues/2642)

Bugfixes - 20.24.4

•
Upgrade embedded wheels:

Bugfixes - 20.24.3

•
wheel to 0.41.1 from 0.41.0 (#2622 https://github.com/pypa/virtualenv/issues/2622)

Misc - 20.24.3

Bugfixes - 20.24.2

•
Upgrade embedded wheels:

Bugfixes - 20.24.1

•
Upgrade embedded wheels:
•
pip to 23.2 from 23.1.2 - by @arielkirkwood https://github.com/arielkirkwood (#2611 https://github.com/pypa/virtualenv/issues/2611)

Features - 20.24.0

•
Export the prompt prefix as VIRTUAL_ENV_PROMPT when activating a virtual environment - by @jimporter https://github.com/jimporter. (#2194 https://github.com/pypa/virtualenv/issues/2194)

Bugfixes - 20.24.0

•
setuptools to 68.0.0 from 67.8.0 (#2607 https://github.com/pypa/virtualenv/issues/2607)

Bugfixes - 20.23.1

•
setuptools to 67.8.0 from 67.7.2 (#2588 https://github.com/pypa/virtualenv/issues/2588)

Features - 20.23.0

•
Do not install wheel and setuptools seed packages for Python 3.12+. To restore the old behavior use:
  • for wheel use VIRTUALENV_WHEEL=bundle environment variable or --wheel=bundle CLI flag,
  • for setuptools use VIRTUALENV_SETUPTOOLS=bundle environment variable or --setuptools=bundle CLI flag.

By @chrysle https://github.com/chrysle. (#2487 https://github.com/pypa/virtualenv/issues/2487)

•
3.12 support - by @gaborbernat https://github.com/gaborbernat. (#2558 https://github.com/pypa/virtualenv/issues/2558)

Bugfixes - 20.23.0

Features - 20.22.0

•
Drop support for creating Python <=3.6 (including 2) interpreters. Removed pip of 20.3.4, 21.3.1; wheel of 0.37.1; setuptools of 59.6.0, 44.1.1, 50.3.2- by @gaborbernat https://github.com/gaborbernat. (#2548 https://github.com/pypa/virtualenv/issues/2548)

Bugfixes - 20.21.1

Features - 20.21.0

•
Make closure syntax explicitly starts with {||. (#2512 https://github.com/pypa/virtualenv/issues/2512)

Bugfixes - 20.21.0

Features - 20.20.0

•
Change environment variable existence check in Nushell activation script to not use deprecated command. (#2506 https://github.com/pypa/virtualenv/issues/2506)

Bugfixes - 20.20.0

Features - 20.19.0

•
Allow platformdirs version 3 - by @cdce8p https://github.com/cdce8p. (#2499 https://github.com/pypa/virtualenv/issues/2499)

Features - 20.18.0

•
Drop 3.6 runtime support (can still create 2.7+) - by @gaborbernat https://github.com/gaborbernat. (#2489 https://github.com/pypa/virtualenv/issues/2489)

Bugfixes - 20.18.0

Bugfixes - 20.17.1

Features - 20.17.0

Bugfixes - 20.17.0

Bugfixes - 20.16.7

Features - 20.16.6

•
Drop unneeded shims for PyPy3 directory structure (#2426 https://github.com/pypa/virtualenv/issues/2426)

Bugfixes - 20.16.6

Bugfixes - 20.16.5

•
Do not turn echo off for subsequent commands in batch activators (activate.bat and deactivate.bat) - by @pawelszramowski https://github.com/pawelszramowski. (#2411 https://github.com/pypa/virtualenv/issues/2411)

Bugfixes - 20.16.4

•
Bump embed setuptools to 65.3 - by @gaborbernat https://github.com/gaborbernat. (#2405 https://github.com/pypa/virtualenv/issues/2405)

Bugfixes - 20.16.3

•
Upgrade embedded pip to 22.2.2 from 22.2.1 and setuptools to 63.4.1 from 63.2.0 - by @gaborbernat https://github.com/gaborbernat. (#2395 https://github.com/pypa/virtualenv/issues/2395)

Bugfixes - 20.16.2

•
Bump embedded pip from 22.2 to 22.2.1 - by @gaborbernat https://github.com/gaborbernat. (#2391 https://github.com/pypa/virtualenv/issues/2391)

Features - 20.16.1

•
Update Nushell activation scripts to version 0.67 - by @kubouch https://github.com/kubouch. (#2386 https://github.com/pypa/virtualenv/issues/2386)

Features - 20.16.0

Bugfixes - 20.15.1

Features - 20.15.0

•
Support for Windows embeddable Python package: includes python<VERSION>.zip in the creator sources - by @reksarka https://github.com/reksarka. (#1774 https://github.com/pypa/virtualenv/issues/1774)

Bugfixes - 20.15.0

Features - 20.14.1

•
Support for creating a virtual environment from a Python 2.7 framework on macOS 12 - by @nickhutchinson https://github.com/nickhutchinson. (#2284 https://github.com/pypa/virtualenv/issues/2284)

Bugfixes - 20.14.1

•
Upgrade embedded setuptools to 62.1.0 from 61.0.0 - by @gaborbernat https://github.com/gaborbernat. (#2327 https://github.com/pypa/virtualenv/issues/2327)

Features - 20.14.0

•
Support Nushell activation scripts with nu version 0.60 - by @kubouch https://github.com/kubouch. (#2321 https://github.com/pypa/virtualenv/issues/2321)

Bugfixes - 20.14.0

•
Upgrade embedded setuptools to 61.0.0 from 60.10.0 - by @gaborbernat https://github.com/gaborbernat. (#2322 https://github.com/pypa/virtualenv/issues/2322)

Bugfixes - 20.13.4

Bugfixes - 20.13.3

Bugfixes - 20.13.2

•
Upgrade embedded setuptools to 60.9.3 from 60.6.0 - by @gaborbernat https://github.com/gaborbernat. (#2306 https://github.com/pypa/virtualenv/issues/2306)

Bugfixes - 20.13.1

Features - 20.13.0

•
Add downloaded wheel information in the relevant JSON embed file to prevent additional downloads of the same wheel. - by @mayeut https://github.com/mayeut. (#2268 https://github.com/pypa/virtualenv/issues/2268)

Bugfixes - 20.13.0

Bugfixes - 20.12.1

Features - 20.12.0

•
Sign the python2 exe on Darwin arm64 - by @tmspicer https://github.com/tmspicer. (#2233 https://github.com/pypa/virtualenv/issues/2233)

Bugfixes - 20.12.0

Bugfixes - 20.11.2

•
Fix installation of pinned versions of pip, setuptools & wheel - by @mayeut https://github.com/mayeut. (#2203 https://github.com/pypa/virtualenv/issues/2203)

Bugfixes - 20.11.1

•
Bump embed setuptools to 60.1.1 from 60.1.0 - by @gaborbernat https://github.com/gaborbernat. (#2258 https://github.com/pypa/virtualenv/issues/2258)

Features - 20.11.0

Features - 20.10.0

  • If a "venv" install scheme exists in sysconfig, virtualenv now uses it to create new virtual environments. This allows Python distributors, such as Fedora, to patch/replace the default install scheme without affecting the paths in new virtual environments. A similar technique was proposed to Python, for the venv module https://bugs.python.org/issue45413 - by hroncok (#2208 https://github.com/pypa/virtualenv/issues/2208)
  • The activated virtualenv prompt is now always wrapped in parentheses. This affects venvs created with the --prompt attribute, and matches virtualenv's behavior on par with venv. (#2224 https://github.com/pypa/virtualenv/issues/2224)

Bugfixes - 20.10.0

•
Fix broken prompt set up by activate.bat - by @SiggyBar https://github.com/SiggyBar. (#2225 https://github.com/pypa/virtualenv/issues/2225)

Features - 20.9.0

Bugfixes - 20.9.0

Bugfixes - 20.8.1

Misc - 20.8.1

  • upgrade embedded setuptools to 58.0.4 from 57.4.0 and pip to 21.2.4 from 21.2.3
  • Add nushell activation script

Bugfixes - 20.7.2

•
Upgrade embedded pip to 21.2.3 from 21.2.2 and wheel to 0.37.0 from 0.36.2 - by @gaborbernat https://github.com/gaborbernat. (#2168 https://github.com/pypa/virtualenv/issues/2168)

Bugfixes - 20.7.1

•
Fix unpacking dictionary items in PythonInfo.install_path (#2165 https://github.com/pypa/virtualenv/issues/2165)

Bugfixes - 20.7.0

•
upgrade embedded pip to 21.2.2 from 21.1.3 and setuptools to 57.4.0 from 57.1.0 - by @gaborbernat https://github.com/gaborbernat (#2159 https://github.com/pypa/virtualenv/issues/2159)

Deprecations and Removals - 20.7.0

•
Removed xonsh activator due to this breaking fairly often the CI and lack of support from those packages maintainers, upstream is encouraged to continue supporting the project as a plugin https://github.com/xonsh/xonsh/issues/3689 - by @gaborbernat https://github.com/gaborbernat. (#2160 https://github.com/pypa/virtualenv/issues/2160)

Features - 20.6.0

•
Support Python interpreters without distutils (fallback to syconfig in these cases) - by @gaborbernat https://github.com/gaborbernat. (#1910 https://github.com/pypa/virtualenv/issues/1910)

Features - 20.5.0

Bugfixes - 20.5.0

•
Bump pip the embedded pip 21.1.3 and setuptools to 57.1.0 - by @gaborbernat https://github.com/gaborbernat. (#2135 https://github.com/pypa/virtualenv/issues/2135)

Deprecations and Removals - 20.5.0

•
Drop python 3.4 support as it has been over 2 years since EOL - by @gaborbernat https://github.com/gaborbernat. (#2141 https://github.com/pypa/virtualenv/issues/2141)

Bugfixes - 20.4.7

•
Upgrade embedded pip to 21.1.2 and setuptools to 57.0.0 - by @gaborbernat https://github.com/gaborbernat. (#2123 https://github.com/pypa/virtualenv/issues/2123)

Bugfixes - 20.4.6

•
Fix site.getsitepackages() broken on python2 on debian - by @freundTech https://github.com/freundTech. (#2105 https://github.com/pypa/virtualenv/issues/2105)

Bugfixes - 20.4.5

Bugfixes - 20.4.4

Bugfixes - 20.4.3

Bugfixes - 20.4.2

•
Running virtualenv --upgrade-embed-wheels crashes - by @gaborbernat https://github.com/gaborbernat. (#2058 https://github.com/pypa/virtualenv/issues/2058)

Bugfixes - 20.4.1

•
Bump embedded pip and setuptools packages to latest upstream supported (21.0.1 and 52.0.0) - by @gaborbernat https://github.com/gaborbernat. (#2060 https://github.com/pypa/virtualenv/issues/2060)

Features - 20.4.0

•
On the programmatic API allow passing in the environment variable dictionary to use, defaults to os.environ if not specified - by @gaborbernat https://github.com/gaborbernat. (#2054 https://github.com/pypa/virtualenv/issues/2054)

Bugfixes - 20.4.0

•
Upgrade embedded setuptools to 51.3.3 from 51.1.2 - by @gaborbernat https://github.com/gaborbernat. (#2055 https://github.com/pypa/virtualenv/issues/2055)

Bugfixes - 20.3.1

Features - 20.3.0

•
The builtin discovery takes now a --try-first-with argument and is first attempted as valid interpreters. One can use this to force discovery of a given python executable when the discovery order/mechanism raises errors - by @gaborbernat https://github.com/gaborbernat. (#2046 https://github.com/pypa/virtualenv/issues/2046)

Bugfixes - 20.3.0

Bugfixes - 20.2.2

•
Bump pip to 20.3.1, setuptools to 51.0.0 and wheel to 0.36.1 - by @gaborbernat https://github.com/gaborbernat. (#2029 https://github.com/pypa/virtualenv/issues/2029)

No significant changes.

Features - 20.2.0

  • Optionally skip VCS ignore directive for entire virtualenv directory, using option no-vcs-ignore <#no-vcs-ignore>, by default False. (#2003 https://github.com/pypa/virtualenv/issues/2003)
  • Add --read-only-app-data option to allow for creation based on an existing app data cache which is non-writable. This may be useful (for example) to produce a docker image where the app-data is pre-populated.
ENV \
    VIRTUALENV_OVERRIDE_APP_DATA=/opt/virtualenv/cache \
    VIRTUALENV_SYMLINK_APP_DATA=1
RUN virtualenv venv && rm -rf venv
ENV VIRTUALENV_READ_ONLY_APP_DATA=1
USER nobody
# this virtualenv has symlinks into the read-only app-data cache
RUN virtualenv /tmp/venv

Patch by @asottile https://github.com/asottile. (#2009 https://github.com/pypa/virtualenv/issues/2009)

Bugfixes - 20.2.0

•
Fix processing of the VIRTUALENV_PYTHON environment variable and make it multi-value as well (separated by comma) - by @pneff https://github.com/pneff. (#1998 https://github.com/pypa/virtualenv/issues/1998)

Features - 20.1.0

•
The python specification can now take one or more values, first found is used to create the virtual environment - by @gaborbernat https://github.com/gaborbernat. (#1995 https://github.com/pypa/virtualenv/issues/1995)

Bugfixes - 20.0.35

Bugfixes - 20.0.34

Bugfixes - 20.0.33

Bugfixes - 20.0.32

Bugfixes - 20.0.31

•
Upgrade embedded pip to 20.2.1, setuptools to 49.6.0 and wheel to 0.35.1 - by @gaborbernat https://github.com/gaborbernat. (#1918 https://github.com/pypa/virtualenv/issues/1918)

Bugfixes - 20.0.30

•
Upgrade pip to 20.2.1 and setuptools to 49.2.1 - by @gaborbernat https://github.com/gaborbernat. (#1915 https://github.com/pypa/virtualenv/issues/1915)

Bugfixes - 20.0.29

•
Upgrade embedded pip from version 20.1.2 to 20.2 - by @gaborbernat https://github.com/gaborbernat. (#1909 https://github.com/pypa/virtualenv/issues/1909)

Bugfixes - 20.0.28

Bugfixes - 20.0.27

Bugfixes - 20.0.26

  • better logging output while running and enable logging on background process call ( _VIRTUALENV_PERIODIC_UPDATE_INLINE may be used to debug behavior inline)
  • fallback to unverified context when querying the PyPi for release date,
  • stop downloading wheels once we reach the embedded version,

by @gaborbernat https://github.com/gaborbernat. (#1883 https://github.com/pypa/virtualenv/issues/1883)

Bugfixes - 20.0.25

•
Fix that when the app-data seeders image creation fails the exception is silently ignored. Avoid two virtual environment creations to step on each others toes by using a lock while creating the base images. By @gaborbernat https://github.com/gaborbernat. (#1869 https://github.com/pypa/virtualenv/issues/1869)

Features - 20.0.24

•
Ensure that the seeded packages do not get too much out of date:
  • add a CLI flag that triggers upgrade of embedded wheels under upgrade-embed-wheels <#upgrade-embed-wheels>
  • periodically (once every 14 days) upgrade the embedded wheels in a background process, and use them if they have been released for more than 28 days (can be disabled via no-periodic-update <#no-periodic-update>)

More details under Wheel acquisition <#wheels> - by @gaborbernat https://github.com/gaborbernat. (#1821 https://github.com/pypa/virtualenv/issues/1821)

•
Upgrade embed wheel content:
  • ship wheels for Python 3.9 and 3.10
  • upgrade setuptools for Python 3.5+ from 47.1.1 to 47.3.1

by @gaborbernat https://github.com/gaborbernat. (#1841 https://github.com/pypa/virtualenv/issues/1841)

•
Display the installed seed package versions in the final summary output, for example:
created virtual environment CPython3.8.3.final.0-64 in 350ms
  creator CPython3Posix(dest=/x, clear=True, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/y/virtualenv)
    added seed packages: pip==20.1.1, setuptools==47.3.1, wheel==0.34.2

by @gaborbernat https://github.com/gaborbernat. (#1864 https://github.com/pypa/virtualenv/issues/1864)

Bugfixes - 20.0.24

Improved Documentation - 20.0.24

Bugfixes - 20.0.23

•
Fix typo in setup.cfg - by @RowdyHowell https://github.com/RowdyHowell. (#1857 https://github.com/pypa/virtualenv/issues/1857)

Bugfixes - 20.0.22

Features - 20.0.21

•
Generate ignore file for version control systems to avoid tracking virtual environments by default. Users should remove these files if still want to track. For now we support only git by @gaborbernat https://github.com/gaborbernat. (#1806 https://github.com/pypa/virtualenv/issues/1806)

Bugfixes - 20.0.21

Bugfixes - 20.0.20

Bugfixes - 20.0.19

Bugfixes - 20.0.18

•
Importing setuptools before cli_run could cause our python information query to fail due to setuptools patching distutils.dist.Distribution - by @gaborbernat https://github.com/gaborbernat. (#1771 https://github.com/pypa/virtualenv/issues/1771)

Features - 20.0.17

•
Extend environment variables checked for configuration to also check aliases (e.g. setting either VIRTUALENV_COPIES or VIRTUALENV_ALWAYS_COPY will work) - by @gaborbernat https://github.com/gaborbernat. (#1763 https://github.com/pypa/virtualenv/issues/1763)

Bugfixes - 20.0.16

•
Allow seed wheel files inside the extra-search-dir <#extra-search-dir> folders that do not have Requires-Python metadata specified, these are considered compatible with all python versions - by @gaborbernat https://github.com/gaborbernat. (#1757 https://github.com/pypa/virtualenv/issues/1757)

Features - 20.0.15

•
Upgrade embedded setuptools to 46.1.3 from 46.1.1 - by @gaborbernat https://github.com/gaborbernat. (#1752 https://github.com/pypa/virtualenv/issues/1752)

Features - 20.0.14

Bugfixes - 20.0.14

•
Fix discovery of interpreter by name from PATH that does not match a spec format - by @gaborbernat https://github.com/gaborbernat. (#1746 https://github.com/pypa/virtualenv/issues/1746)

Bugfixes - 20.0.13

Bugfixes - 20.0.12

•
Fix relative path discovery of interpreters - by @gaborbernat https://github.com/gaborbernat. (#1734 https://github.com/pypa/virtualenv/issues/1734)

Features - 20.0.11

•
Improve error message when the host python does not satisfy invariants needed to create virtual environments (now we print which host files are incompatible/missing and for which creators when no supported creator can be matched, however we found creators that can describe the given Python interpreter - will still print no supported creator for Jython, however print exactly what host files do not allow creation of virtual environments in case of CPython/PyPy) - by @gaborbernat https://github.com/gaborbernat. (#1716 https://github.com/pypa/virtualenv/issues/1716)

Bugfixes - 20.0.11

Improved Documentation - 20.0.11

•
supports <#compatibility-requirements> details now explicitly what Python installations we support - by @gaborbernat https://github.com/gaborbernat. (#1714 https://github.com/pypa/virtualenv/issues/1714)

Bugfixes - 20.0.10

Improved Documentation - 20.0.10

Bugfixes - 20.0.9

Bugfixes - 20.0.8

Bugfixes - 20.0.7

•
Disable distutils fixup for python 3 until pypa/pip #7778 https://github.com/pypa/pip/issues/7778 is fixed and released - by @gaborbernat https://github.com/gaborbernat. (#1669 https://github.com/pypa/virtualenv/issues/1669)

Bugfixes - 20.0.6

  • the application data folder is now controllable via app-data <#app-data>,
  • clear-app-data now cleans the entire application data folder, not just the app-data seeder path,
  • check if the application data path passed in does not exist or is read-only, and fallback to a temporary directory,
  • temporary directory application data is automatically cleaned up at the end of execution,
  • symlink-app-data <#symlink-app-data> is always False when the application data is temporary

by @gaborbernat https://github.com/gaborbernat. (#1640 https://github.com/pypa/virtualenv/issues/1640)

Features - 20.0.5

Bugfixes - 20.0.5

Features - 20.0.4

•
When aliasing interpreters, use relative symlinks - by @asottile https://github.com/asottile. (#1596 https://github.com/pypa/virtualenv/issues/1596)

Bugfixes - 20.0.4

Bugfixes - 20.0.3

Improved Documentation - 20.0.3

•
Document a programmatic API as from virtualenv import cli_run under Python <#programmatic-api> - by @gaborbernat https://github.com/gaborbernat. (#1585 https://github.com/pypa/virtualenv/issues/1585)

Features - 20.0.2

Bugfixes - 20.0.2

  • do not fail if there are executables that fail to query (e.g. for not having execute access to it) on the PATH,
  • beside the prefix folder also try with the platform dependent binary folder within that,

by @gaborbernat https://github.com/gaborbernat. (#1545 https://github.com/pypa/virtualenv/issues/1545)

  • When copying (either files or trees) do not copy the permission bits, last access time, last modification time, and flags as access to these might be forbidden (for example in case of the macOs Framework Python) and these are not needed for the user to use the virtual environment - by @gaborbernat https://github.com/gaborbernat. (#1561 https://github.com/pypa/virtualenv/issues/1561)
  • While discovering a python executables interpreters that cannot be queried are now displayed with info level rather than warning, so now they're no longer shown by default (these can be just executables to which we don't have access or that are broken, don't warn if it's not the target Python we want) - by @gaborbernat https://github.com/gaborbernat. (#1574 https://github.com/pypa/virtualenv/issues/1574)
  • The app-data seeder <#seeder> no longer symlinks the packages on UNIX and copies on Windows. Instead by default always copies, however now has the symlink-app-data <#symlink-app-data> flag allowing users to request this less robust but faster method - by @gaborbernat https://github.com/gaborbernat. (#1575 https://github.com/pypa/virtualenv/issues/1575)

Improved Documentation - 20.0.2

Features - 20.0.1

•
upgrade embedded setuptools to 45.2.0 from 45.1.0 for Python 3.4+ - by @gaborbernat https://github.com/gaborbernat. (#1554 https://github.com/pypa/virtualenv/issues/1554)

Bugfixes - 20.0.1

Improved Documentation - 20.0.0.

•
Fixes typos, repeated words and inconsistent heading spacing. Rephrase parts of the development documentation and CLI documentation. Expands shorthands like env var and config to their full forms. Uses descriptions from respective documentation, for projects listed in related links - by @pradyunsg https://github.com/pradyunsg. (#1540 https://github.com/pypa/virtualenv/issues/1540)

Features - 20.0.0b2

•
Improve base executable discovery mechanism:
  • print at debug level why we refuse some candidates,
  • when no candidates match exactly, instead of hard failing fallback to the closest match where the priority of matching attributes is: python implementation, major version, minor version, architecture, patch version, release level and serial (this is to facilitate things to still work when the OS upgrade replace/upgrades the system python with a never version, than what the virtualenv host python was created with),
  • always resolve system_executable information during the interpreter discovery, and the discovered environment is the system interpreter instead of the venv/virtualenv (this happened before lazily the first time we accessed, and caused reporting that the created virtual environment is of type of the virtualenv host python version, instead of the system pythons version - these two can differ if the OS upgraded the system python underneath and the virtualenv host was created via copy),

by @gaborbernat https://github.com/gaborbernat. (#1515 https://github.com/pypa/virtualenv/issues/1515)

Bugfixes - 20.0.0b2

  • no longer shows accepted interpreters information (as the last proposed one is always the accepted one),
  • do not display the str_spec attribute for PythonSpec as these can be deduced from the other attributes,
  • for the app-data seeder do not show the type of lock, only the path to the app data directory,

By @gaborbernat https://github.com/gaborbernat. (#1510 https://github.com/pypa/virtualenv/issues/1510)

Improved Documentation - 20.0.0b2

Warning:

The current virtualenv is the second iteration of implementation. From version 0.8 all the way to 16.7.9 we numbered the first iteration. Version 20.0.0b1 is a complete rewrite of the package, and as such this release history starts from there. The old changelog is still available in the legacy branch documentation https://virtualenv.pypa.io/en/legacy/changes.html.

Author name not set

2007-2026, PyPA, PyPA

October 3, 2026 21.14