VIRTUALENV(1)
π VIRTUALENV(1) π
okay bestie, this is the virtualenv page β section 1 of the manual, VIRTUALENV(1), virtualenv β and we're doing the whole thing, no skipping, no cap π
πΈ NAME πΈ
virtualenv β virtualenv 21.14.7 π
okay so first, the badge wall, because of course there's a badge wall (iconic behavior honestly). Here's every single one, with where it points:
[image: Latest version on PyPI]β https://pypi.org/project/virtualenv/#history[image: PyPI - Implementation][image: PyPI - Python Version][image: Documentation status]β https://virtualenv.pypa.io[image: Discord]β https://discord.gg/pypa[image: PyPI - Downloads]β https://pypistats.org/packages/virtualenv[image: PyPI - License]β https://opensource.org/licenses/MIT[image: Open issues]β https://github.com/pypa/virtualenv/issues[image: Open pull requests]β https://github.com/pypa/virtualenv/pulls[image: Package popularity]β https://pypistats.org/packages/virtualenv
and now the actual sentence, because that's the important part:
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 <>. β¨
π§ QUICK NAVIGATION π§
like a map of the whole docs site, bestie. take notes π«Ά
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
π RELATED PROJECTS π
several tools build on virtualenv to provide higher-level workflows (yes, it's giving ecosystem π):
- virtualenvwrapper β https://virtualenvwrapper.readthedocs.io/en/latest/ β Shell wrapper for creating and managing multiple virtualenvs
- pew β https://github.com/berdario/pew β Python Env Wrapper, a set of commands to manage multiple virtual environments
- tox β https://tox.readthedocs.io/en/latest/ β Automate testing across multiple Python versions
- nox β https://nox.thea.codes/en/stable/ β Flexible test automation in Python
π EXTERNAL RESOURCES π
learn more about virtualenv from these community resources:
- Corey Schafer's virtualenv tutorial β https://www.youtube.com/watch?v=N5vscPTWKOk β Video walkthrough for beginners
- Bernat Gabor's status quo β https://www.youtube.com/watch?v=o1Vue9CWRxU β Talk about the current state of Python packaging
- Carl Meyer's reverse-engineering β https://pyvideo.org/video/568/reverse-engineering-ian-bicking--39-s-brain--insi/ β Deep dive into how virtualenv works internally
π 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. genuinely the fun part, fr fr π
β¨ Prerequisites β¨
Before starting this tutorial, you need:
- Python 3.9 or later installed on your system. If you use a version manager like pyenv (https://github.com/pyenv/pyenv), mise (https://mise.jdx.dev/), or asdf (https://asdf-vm.com/),
virtualenvwill automatically discover the Python version they manage. slay. virtualenvinstalled (see Install virtualenv<>).
π¦ Create your first virtual environment π¦
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. literally its own little world π«
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.
π Activate the environment π
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
π Install a package π
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. the way that path just tells you ππ
π Deactivate π
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. no activation needed, we simply vibe π
πΈ Set up a real project πΈ
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]. π₯
π What you learned π
tiny heads-up bestie: the source page has a[graph]sitting right before this heading (the.SSis just an artifact of the conversion, we don't need to stress about it π).
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
.venvredirect file.
β Next steps β
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 tovenv. - 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.
π Install virtualenv π
virtualenv is a command-line tool, so it should be installed in an isolated environment rather than into your system Python (please don't dump it in system Python, i'm begging π). 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
--userto avoid modifying system packages. May not work on distributions with externally-managed Python environments.
β οΈ that last one is the "it depends" option β externally-managed Python environments can absolutely say no to you, and that's on purpose π«Ά
zipappβ 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:
$ uv tool install virtualenv
and the development version, for when you like living on the edge:
$ uv tool install git+https://github.com/pypa/virtualenv.git@main
[pipx]β install virtualenv using pipx:
$ pipx install virtualenv
dev version:
$ pipx install git+https://github.com/pypa/virtualenv.git@main
[pip]β install virtualenv using pip:
$ python -m pip install --user virtualenv
dev 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, useuv toolorpipxinstead. π
[zipapp]β download the zipapp file and run it directly:
$ python virtualenv.pyz --help
Grab 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. π
π VERIFY INSTALLATION
Check the installed version, bestie:
$ virtualenv --version
See Compatibility <> for supported Python versions.
π USE VIRTUALENV
Okay so this is the fun part β actually using the thing. β¨
π Select a Python version
By default, virtualenv uses the same Python version it runs under. Override this with --python or -p.
Using version specifiers
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
Using PEP 440 specifiers
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.
Using free-threading Python
Create an environment with free-threading Python <https://docs.python.org/3/howto/free-threading-python.html>:
$ virtualenv -p 3.13t venv
Targeting a specific CPU architecture
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). π¦
Using absolute paths
Specify the full path to a Python interpreter:
$ virtualenv -p /usr/bin/python3.9 venv
Using --try-first-with
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. Sad for it, honestly. π
Using version managers (pyenv, mise, asdf)
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. No cap, it just figures it out. π₯
Activate a virtual environment
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
Deactivate the environment
Exit the virtual environment:
$ deactivate
Bye bestie π
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.
Customize prompt
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. Cute, right? π
Programmatic activation
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()
βοΈ CONFIGURE DEFAULTS
Use a configuration file to set default options for virtualenv. Set it once, never think about it again. iconic. β¨
Configuration file location
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.
Configuration format
Derive configuration keys from command-line options by stripping leading - and replacing remaining - with _:
[virtualenv]
python = /opt/python-3.14/bin/python
Multi-value options
Specify multiple values on separate lines:
[virtualenv]
extra_search_dir =
/path/to/dists
/path/to/other/dists
Environment variables
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.
Override app-data location
Set the VIRTUALENV_OVERRIDE_APP_DATA environment variable to override the default app-data cache directory location.
Allow unverified HTTPS for periodic updates
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. π¨
Configuration priority
Options are resolved in this order (highest to lowest priority):
[graph]
π§ 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. Heads up! π
Point a tool at the right environment
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.
Choose once for every environment
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. π
Commit the file or ignore it
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.
π± CONTROL SEED PACKAGES
Upgrade embedded wheels
Update the embedded wheel files to the latest versions:
$ virtualenv --upgrade-embed-wheels
Provide custom wheels
Use custom wheel files from a local directory:
$ virtualenv --extra-search-dir /path/to/wheels venv
Download latest from PyPI
Download the latest versions of seed packages from PyPI:
$ virtualenv --download venv
Disable periodic updates
Disable automatic periodic updates of seed packages:
$ virtualenv --no-periodic-update venv
For distribution maintainers
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. π«Ά
π Find out which Python an environment uses
Bestie, do not run the environment's interpreter just to answer this question β that costs you a whole subprocess and it literally fails the moment the base Python has been removed π. Instead, read python-version straight out of pyvenv.cfg:
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'
So pyvenv.cfg has no section header, which is why we slap that [cfg] prefix on the front β iykyk π. That key holds the feature release only, so if you need the patch level too, reach for version instead.
π Use from Python code
You can call virtualenv from Python code with the cli_run function:
from virtualenv import cli_run
cli_run(["venv"])
Pass options as list elements, one per element:
cli_run(["-p", "python3.14", "--without-pip", "myenv"])
Then use the session object it hands back to peek at environment details:
result = cli_run(["venv"])
print(result.creator.dest) # path to created environment
print(result.creator.exe) # path to python executable
And if you just want to describe the environment without actually creating it, that's session_via_cli energy:
from virtualenv import session_via_cli
session = session_via_cli(["venv"])
# inspect session.creator, session.seeder, session.activators
See Python <> for the complete API documentation β¨.
π Verify a virtualenv release
Okay so the release.yaml workflow in pypa/virtualenv (https://github.com/pypa/virtualenv) builds and publishes every single release π. The steps below check that a file you downloaded genuinely came out of that workflow, and they also show you what the release bundles contain. The examples use release 21.12.1 β swap in whatever version you actually have, bestie. Release artifacts <> lists every file a release publishes, and Release integrity #release-integrity covers what these checks actually prove π«Ά.
You'll need uv (https://docs.astral.sh/uv/) β its uvx runs pip and pypi-attestations without installing them β plus pypi-attestations (https://pypi.org/project/pypi-attestations/), the GitHub CLI (https://cli.github.com) for the GitHub release assets and attestations, and jq (https://jqlang.org) to read the SBOMs πΈ.
π¦ Verify a wheel or sdist from PyPI
Download the wheel and the sdist without installing them, like a good little investigator:
$ uvx pip download virtualenv==21.12.1 --no-deps --dest .
$ uvx pip download virtualenv==21.12.1 --no-deps --no-binary :all: --dest .
Now check each file against the attestation PyPI stores for it β one file per call, no multitasking π:
$ 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 some other repository fails with provenance was signed by repository .... π. If you want to check the copy on PyPI without downloading it first, just prefix the file name with pypi:, like pypi:virtualenv-21.12.1-py3-none-any.whl.
π Verify the GitHub release and assets
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 it 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!
Next, check a file you downloaded against the digests the attestation lists. The command hashes the local file, so it totally 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
Now, GitHub signs the release attestation itself, so it proves the assets didn't change after publication β but it does not name the workflow that built them. The provenance check below is the one that does that, bestie π.
π¦ Verify the zipapp
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 π. And 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 actually have π«Ά. Verify it with gh release verify-asset for that release, or with gh attestation verify as above, leaving out --bundle.
Heads up: releases before 21.7.11 have neither a release attestation nor a provenance bundle π β so instead, compare the file's SHA-256 with the digest GitHub lists for the release asset:
$ 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
π§Ύ Read the embedded SBOM
The wheel carries a CycloneDX (https://cyclonedx.org) SBOM that lists the pip and setuptools wheels virtualenv bundles β with their hashes, licenses, and which 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 that 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'))"
π‘οΈ Verify the SBOM attestations
The release workflow attests the SBOM against the wheel and the sdist. Check that the 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, which is the most satisfying silence ever, ngl π:
$ 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 they 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 must match 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)
π Read the zipapp SBOM
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 that 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
π Rebuild the release files
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 β iconic, honestly π.
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
Okay so releases after 21.13.0 also pin the backend in tasks/release-requirements.txt, which means on those tags --build-constraints tasks/release-requirements.txt --require-hashes swaps in for the file you read from the SBOM. π
And bestie β build from a git checkout, because the SBOM records the source commit and an sdist literally does not carry it. Wheels up to 21.10.0 recorded the machine that built them in the SBOM, so rebuilding one of those gives you a diff 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 too, no cap. β¨ The pylock.zipapp.toml lock at the tag (over at https://github.com/pypa/virtualenv/blob/main/pylock.zipapp.toml) pins each distribution it bundles by version and SHA-256, so literally 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 that -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 rebuilding one of those takes its constraints from that file and needs neither SBOM. Slay. π
π COMPATIBILITY π
Supported Python implementations
virtualenv works with the following Python interpreter implementations, bestie. π Only the latest patch version of each minor version is fully supported β previous patch versions work on a best-effort basis, iykyk. π«Ά
CPython
3.15 >= python_version >= 3.9
PyPy
3.11 >= python_version >= 3.9
GraalPy
24.1 and later (Linux and macOS only).
RustPython
Experimental support (Linux, macOS, and Windows). RustPython (over at https://github.com/RustPython/RustPython) implements Python 3.14. π¦
Support policy
- New versions are added close to their release date β typically during the beta phase. β
- Old versions get dropped 18 months after CPython EOL (see
https://devguide.python.org/versions/), which gives users plenty of time to migrate. π
Version support timeline
The major version support changes, in order:
21.5.0(2026-06-13): dropped support for running under and creating environments for Python3.8and earlier.20.27.0(2024-10-17): dropped support for running under Python3.7and earlier.20.22.0(2023-04-19): dropped support for creating environments for Python3.6and earlier.20.18.0(2023-02-06): dropped support for running under Python3.6and earlier.
Supported operating systems
CPython ships in multiple forms, and each OS repackages it β often applying some customization. π€ The platforms listed below are the tested ones. Unlisted platforms may work but are not explicitly supported. If you hit issues on an unlisted platform, please open a feature request. π
Cross-platform
These Python distributions work on Linux, macOS, and Windows:
- Installations from python.org (
https://www.python.org/downloads/) python-build-standalonebuilds (https://github.com/astral-sh/python-build-standalone) β the ones used byuv(https://docs.astral.sh/uv/) andmise(https://mise.jdx.dev/)- Python versions managed by
pyenv(https://github.com/pyenv/pyenv),mise(https://mise.jdx.dev/), orasdf(https://asdf-vm.com/) β shims are automatically resolved to the real binary, no worries π
Linux
- Ubuntu
16.04and later (both upstream anddeadsnakesbuilds βhttps://launchpad.net/~deadsnakes/+archive/ubuntu/ppa) - Fedora
- RHEL and CentOS
- OpenSuse
- Arch Linux
macOS
- Python versions installed via Homebrew (
https://docs.brew.sh/Homebrew-and-Python) β works, but not recommended (https://justinmayer.com/posts/homebrew-python-is-not-for-you/). Homebrew may upgrade or remove Python versions without warning and break your existing virtual environments. π¨ - The Python 3 that ships as part of XCode (Python framework builds at
/Library/Frameworks/Python3.framework/)
Note: framework builds do not support copy-based virtual environments. Use thesymlinkorhardlinkcreation methods instead. π
Windows
- Windows Store Python
3.9and later (https://apps.microsoft.com/search?query=python)
β¨ COMMAND LINE β¨
virtualenv is primarily a command line application. All options have sensible defaults, and there's exactly 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. π§
Command line options
virtualenv [OPTIONS]
Named Arguments πΈ
--versionβ default'==SUPPRESS=='β display the version of thevirtualenvpackage and its location, then exit--with-tracebackβ defaultFalseβ on failure also display the stacktrace internals ofvirtualenv--read-only-app-dataβ defaultFalseβ use the app data folder in read-only mode (write operations will fail with an error)--app-dataβ default: platform specific application data folder β a data folder used as a cache byvirtualenv--reset-app-dataβ defaultFalseβ start with an empty app data folder--upgrade-embed-wheelsβ defaultFalseβ 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β value2β increase verbosity-q,--quietβ value0β decrease verbosity
β¨ discovery β¨
okay bestie, first up is the core β options shared across all discovery zone β this is where virtualenv figures out which Python is the moment π
--discoveryβ default:'builtin'; interpreter discovery method; choice of:builtin-p,--pythonβ default: 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β default:[]; try first these interpreters before starting the discovery
π creator π
next is the core β options shared across all creator table β this is the actual βmake the environmentβ energy, no cap β¨
--creatorβ default: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,venvdestβ directory to create virtualenv at--clearβ default:False; remove the destination directory if exist before starting (will overwrite files otherwise)--no-vcs-ignoreβ default:False; don't create VCS ignore directive in the destination directory--venv-redirectβ point a PEP-832.venvredirect file next to the destination at the created environment, even outside a project folder; unset, virtualenv does so only beside apyproject.tomlwith no.venvyet--no-venv-redirectβ write no PEP-832.venvredirect file--system-site-packagesβ default:False; give the virtual environment access to the systemsite-packagesdir--symlinksβ default:True; try to use symlinks rather than copies, when symlinks are not the default for the platform--copies,--always-copyβ default:False; try to use copies rather than symlinks, even when symlinks are the default for the platform
β¨ seeder β¨
ok so the seeder is the whole "what packages go into the baby env" situation, and it's got two layers of options β the core ones everybody shares, and then the extra little app-data-only ones. let's go π
core β options shared across all seeder π
--seederβ this picks the seed packages install method, defaults to'app-data', and your choices are:app-data,pip.--no-seed,--without-pipβ defaults toFalse; pass it when you want to not install seed packages at all, like none, zero, nada.--no-download,--never-downloadβ defaults toTrue; pass to disable download of the latest pip/setuptools/wheel from PyPI.--downloadβ defaults toFalse; pass to enable download of the latest pip/setuptools/wheel from PyPI (yes bestie, it's the other side of the coin π ).--extra-search-dirβ defaults to[]; it's a path containing wheels to extend the internal wheel list, and you can set it 1+ times.--pipβ defaults to'bundle'; version of pip to install as seed:embed,bundle,none, or an exact version.--setuptoolsβ defaults to'none'; version of setuptools to install as seed:embed,bundle,none, or an exact version.--no-pipβ defaults toFalse; do not install pip, periodt.--no-setuptoolsβ defaults toFalse; do not install setuptools.--no-periodic-updateβ defaults toFalse; disable the periodic (once every 14 days) update of the embedded wheels ποΈ
app-data β options specific to seeder app-data π¦
--symlink-app-dataβ defaults toFalse; symlink the python packages from the app-data folder (requires seed pip>=19.3). lowkey important, don't skip that version gate β¨
β¨ activators β¨
the activators are literally the little scripts that make your shell go "yes queen, you're in the venv now" π
core β options shared across all activators πΈ
--activatorsβ takes a comma separated list of activators, and activators to generate - default is all supported; choice of:xonsh,bash,batch,cshell,fish,nushell,powershell,python. pick your faves, or let it do everybody π--promptβ provides an alternative prompt prefix for this environment β and iykyk, the value of.means name of the current working directory π
β¨ Environment layout β¨
ok so this is the cute part: 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. no cap, they all point home π«Ά
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. β
POSIX
bin holds one real reference to the host interpreter and links the rest to it:
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}.
πͺ Windows
Okay so on Windows, Scripts holds actual copies instead of links, because symlinking the interpreter over there is just unreliable, periodt (bpo-42013 https://bugs.python.org/issue42013). Here's the whole cast of characters that shows up in there π
python.exeβ references the host interpreterpython3.exeβ an aliaspython3β an alias without the extensionpython3.{minor}t.exeβ free-threaded builds onlypythonw.exeβ runs without opening a console windowpythonw3.exeβ alias forpythonw.exepythonw3.{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 exactly 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.
π Generated files
So when you create an environment, it writes files inside the destination folder and beside it too. This page lists each one, what it holds, and how to suppress it β because bestie, you deserve to know what's landing on your disk π
π Inside the environment folder
pyvenv.cfg
This one 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. Like, fully breaks it. Do not.
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 ngl they differ in precision and in who's supposed to read them π
python-versionβ example3.14. Read it when 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β example3.14.6. Read it when you need the patch level as well. PEP 838 discourages reading it in favor ofpython-version.version_infoβ example3.14.6.final.0. Read it when 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 just skip it. Iconic behaviour honestly. 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. Not our problem, their vibe πΈ
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 π
π§ Beside the environment folder
.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-redirectand--no-venv-redirectin backward incompatible ways. So like... don't build your whole personality on it yet π
py314
okay so when virtualenv actually writes that file it's got a whole little ruleset it follows β think dress code, but for one text line. here's the tea, rule by rule:
- Encoding β
UTF-8without a byte order mark. no BOM, no baggage β¨ - Content β exactly one line, bestie: the destination folder name, relative to the parent folder, followed by a newline. that's the whole file and honestly? iconic minimalism π
- Target β the first environment created in a folder holding a
pyproject.toml; with--venv-redirect, the environment created last, in any folder. .venvfolder β left completely alone, whether it's a folder or a symlink; virtualenv writes nothing when the destination itself is.venv. hands off, we don't touch π«Ά- Existing redirect β kept, unless you pass
--venv-redirectand its target'spyvenv.cfgcarries thevirtualenvkey; one that another tool wrote, or that points at a missing folder, stays either way. no cap, it just leaves it be. - Failure β logged as a warning; the environment is still created. so it'll absolutely tell on itself, but it will not leave you stranded π
π Python
so the primary interface to virtualenv is the command line application β that's the main character. but plot twist: you can also drive it programmatically via the virtualenv.cli_run function and the Session class π₯
see Use virtualenv for usage examples.
π¦ virtualenv module
virtualenv.cli_run(args, options=None, setup_logging=True, env=None)
Create a virtual environment given some command line interface arguments.
Parameters
args(list[str]) β the command line argumentsoptions(VirtualEnvOptions | None) β passing in aVirtualEnvOptionsobject allows return of the parsed optionssetup_logging(bool) βTrueif setup logging handlers,Falseto use handlers already registeredenv(MutableMapping[str, str] | None) β environment variables to use
Return type
Session
Returns
the session object of the creation (its structure for now is experimental and might change on short notice) π
virtualenv.session_via_cli(args, options=None, setup_logging=True, env=None)
Create a virtualenv session β same as cli_run, but this one does not perform the creation. use this if you just want to peek at what the virtual environment would look like, without actually creating it. window shopping, basically π
Parameters
args(list[str]) β the command line argumentsoptions(VirtualEnvOptions | None) β passing in aVirtualEnvOptionsobject allows return of the parsed optionssetup_logging(bool) βTrueif setup logging handlers,Falseto use handlers already registeredenv(MutableMapping[str, str] | None) β environment variables to use
Return type
Session
Returns
the session object of the creation (its structure for now is experimental and might change on short notice) π
π§ Session class
The Session class represents a virtualenv creation session and gives you access to the created environment's properties. she's the whole vibe check on your env π
class virtualenv.run.session.Session(verbosity, app_data, interpreter, creator, seeder, activators)
Represents a virtual environment creation session.
Parameters
verbosity(int)app_data(AppData)interpreter(PythonInfo)creator(Creator)seeder(Seeder)activators(list[Activator])
property verbosity: int β the verbosity of the run.
property interpreter: PythonInfo β create a virtual environment based on this reference interpreter.
property creator: Creator β the creator used to build the virtual environment (must be compatible with the interpreter).
property seeder: Seeder β the mechanism used to provide the seed packages (pip, setuptools, wheel).
property activators: list[Activator] β activators used to generate activations scripts.
π VirtualEnvOptions
Options namespace passed to plugin constructors, populated from the CLI, environment variables, and configuration files. basically the receipts for where every setting came from π§Ύβ¨
class virtualenv.config.cli.parser.VirtualEnvOptions(**kwargs)
Parameters
kwargs(Any)
set_src(key, value, src)
Set an option value and record where it came from. βοΈ
Parameters
key(str) β the option namevalue(Any) β the option valuesrc(str) β the source of the value (e.g."cli","env var","default")
Return type
None
get_source(key)
Return the source that provided a given option value.
Parameters
key(str) β the option name
Return type
str | None
Returns
the source string (e.g. "cli", "env var", "default"), or None if not tracked
property verbosity: int | None β the verbosity level, computed as verbose - quiet, clamped to zero.
Returns
the verbosity level, or None if neither --verbose nor --quiet has been parsed yet π¦
π Release artifacts π
okay so here's the whole drop, bestie β how a release actually gets born and what falls out of it π
A maintainer kicks off 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 then starts the release workflow <https://github.com/pypa/virtualenv/actions/workflows/release.yaml>, which builds and publishes every single file below. {version} stands for the release version, such as 21.10.0. "Verify a virtualenv release" shows you how to check each file, and Release integrity <#release-integrity> explains what the checks actually prove. iykyk π₯
β¨ Files β¨
virtualenv-{version}-py3-none-any.whlβ where: PyPI <https://pypi.org/project/virtualenv/> β contents: the package, with its CycloneDX SBOM under.dist-info/sbomsvirtualenv-{version}.tar.gzβ where: PyPI β contents: the source distributionvirtualenv.pyzβ where: GitHub release <https://github.com/pypa/virtualenv/releases> β contents: the zipapp: virtualenv and its runtime dependencies for every supported Python, with its own SBOM at the archive rootvirtualenv.pyz.intoto.jsonlβ where: GitHub release β contents: the Sigstore bundle holding the zipapp's SLSA provenancevirtualenv.pyz.cdx.jsonβ where: GitHub release β contents: the zipapp's CycloneDX SBOM, the same file the zipapp carriesvirtualenv.cdx.jsonβ where: GitHub release β contents: the wheel's CycloneDX SBOM, the same file the wheel carriesvirtualenv.spdx.jsonβ where: GitHub release β contents: the wheel's SBOM rendered as SPDX 2.3virtualenv.pyzβ where:https://bootstrap.pypa.io/virtualenv.pyzβ contents: the latest release's zipappvirtualenv.pyz for Python {x.y}β where:https://bootstrap.pypa.io/virtualenv/{x.y}/virtualenv.pyzβ contents: the zipapp for Python{x.y}
ngl yes, virtualenv.pyz shows up twice up there β once on the GitHub release, once on the bootstrap site. same name, different address, both iconic π
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. so like... once it's out, it's OUT, periodt π«Ά
π¦ Attestations π¦
- PyPI publish attestation (PEP 740 <https://peps.python.org/pep-0740/>), predicate
https://docs.pypi.org/attestations/publish/v1β subject: wheel, sdist β stored at:https://pypi.org/integrity/virtualenv/{version}/{file}/provenance - SLSA provenance, predicate
https://slsa.dev/provenance/v1β subject:virtualenv.pyzβ stored at: GitHub attestations API andvirtualenv.pyz.intoto.jsonl
receipts for days, no cap πβ¨
- CycloneDX SBOM β predicate
https://cyclonedx.org/bom- covers:
wheel,sdist - lives in: the GitHub attestations API, predicate equal to
virtualenv.cdx.json
- covers:
- SPDX SBOM β predicate
https://spdx.dev/Document/v2.3- covers:
wheel,sdist - lives in: the GitHub attestations API, predicate equal to
virtualenv.spdx.json
- covers:
- CycloneDX SBOM β predicate
https://cyclonedx.org/bom- covers:
virtualenv.pyz - lives in: the GitHub attestations API, predicate equal to
virtualenv.pyz.cdx.json
- covers:
- GitHub release attestation β predicate
https://in-toto.io/attestation/release/v0.2- covers: the tag's commit and every GitHub release asset, from
21.11.0on - lives in: the GitHub attestations API, read by
gh release verify
- covers: the tag's commit and every GitHub release asset, from
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.
π SBOMs π
πΈ Wheel SBOM πΈ
- Format: CycloneDX 1.6 JSON. that's the whole vibe
- 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-infodirectory. The sdist does not carry one. (sdist stays unbothered, no SBOM for it) - Components: every wheel bundled under
virtualenv/seed/wheels/embed, with its SHA-256, license, the packages it vendors, and avirtualenv:seeded-for-pythonproperty 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-Expressioncarries that SPDX expression. Otherwise it lists itsLicensefield andLicense ::classifier names as declared, plus the SPDX ids they map to as concluded when each name maps to a single SPDX license. literally doing license detective work - Build record: the Python version and build backend packages that produced the wheel, the source commit, and the
SOURCE_DATE_EPOCHused for timestamps. Releases up to21.10.0also recorded the operating system, architecture and interpreter build of the build machine. - First release carrying it:
21.8.1. π
π¦ SPDX rendering π¦
virtualenv.spdx.json is the wheel SBOM rendered as SPDX 2.3 JSON for tools that only read 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. bestie they literally made the two docs hold hands π«Ά
π Zipapp SBOM π
- Format: CycloneDX 1.6 JSON, at
virtualenv.pyz.cdx.jsonin 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, avirtualenv:loaded-for-pythonproperty per Python version that imports it, and a SHA-256 per file. Every file in the archive other than the SBOM appears in it. no file left behind, periodt - 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_EPOCHused for timestamps. Packages installed from platform-specific wheels appear without their files, which differ per operating system and architecture.
π Embedded wheel advisories π
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.
heads up fr fr β if you're on Python 3.9 you're lowkey stuck with those advisory-having versions, because the fixed ones refuse to run there. π
β Build reproducibility β
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. iconic behaviour honestly
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.
β¨ Explanation β¨
This page explains the design decisions and concepts behind virtualenv. It focuses on understanding why things work the way they do. the lore, basically π
π virtualenv vs venv vs uv π
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.
Like, <pink>virtualenv</pink> literally occupies a middle ground, bestie: it's faster and more featureful than venv, while staying a pure Python solution with a plugin system for extensibility. π
So here's the vibe check on the whole landscape:
- Performance π¨
venvβ Slowest (60s+); it spawnspip<https://pip.pypa.io> as a subprocess to seed.virtualenvβ Fast; caches pre-built install images, so subsequent creation is< 1second.uv<https://docs.astral.sh/uv/> β Fastest; Rust implementation, milliseconds. Does not seedpip/setuptoolsby default.
- Extensibility π§©
venvβ No plugin system.virtualenvβ Plugin system for discovery, creation, seeding, and activation.uvβ No plugin system.
- Cross-version π
venvβ Only the Python version it runs under.virtualenvβ Any installed Python via auto-discovery (registry, uv-managed,PATH).uvβ Any installed or uv-managed Python.
- Upgradeability π¦
venvβ Tied to Python releases.virtualenvβ Independent via PyPI <https://pypi.org/project/virtualenv/>.uvβ Independent via its own release cycle.
- Programmatic API π οΈ
venvβ Basiccreate()function only.virtualenvβ 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.uvβ Command line only.
- Type annotations π
venvβ Nopy.typedmarker; limited annotations.virtualenvβ Fully typed with PEP 561 <https://peps.python.org/pep-0561/>py.typedmarker; checked by ty <https://docs.astral.sh/ty/>.uvβ Not applicable (Rust binary).
- Best for π
venvβ Zero dependencies, basic needs.virtualenvβ Plugin extensibility, programmatic API, tool compatibility (tox <https://tox.readthedocs.io>, virtualenvwrapper <https://virtualenvwrapper.readthedocs.io>).uvβ Maximum speed, already using uv for package management.
β¨ Supported Python versions β¨
Okay so this is the part where <hotpink>virtualenv</hotpink> is very much giving main character energy: it distinguishes between the host interpreter (the Python that's literally running virtualenv itself) and the target interpreter (the Python you're making the virtual environment for). And when --python isn't specified? The host and target are the same interpreter. No cap. π«Ά
πΈ Host 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. Periodt. π
π¦ Target interpreter
The target interpreter can differ from the host, bestie β <purple>virtualenv</purple> can create virtual environments for any Python interpreter it can discover on the system, provided a matching creator exists. iykyk.
CPythonπ₯- Platforms: Linux, macOS, Windows
- Notes:
3.9+. Free-threaded builds supported on3.13+. Includes macOS framework, Homebrew, Microsoft Store, and Windows debug build support.
PyPyπ- Platforms: Linux, macOS, Windows
- Notes:
3.9+.
GraalPyπ§- Platforms: Linux, macOS, Windows
- Notes:
24.1+. Minimal test coverage, marked experimental.
RustPythonβ- Platforms: Linux, macOS, Windows
- Notes: 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 just reuses the newest bundle β easy. A target older than the oldest bundled version has no compatible bundled wheel, so the bundled seeders refuse it before the environment is created. π In that case, pass --no-seed for an empty environment, or select a seeder that ships wheels for that version.
Lowkey important: that refusal happens before creation, so you're not left with some cursed half-built environment. π
π How virtualenv works
Python packaging often faces a fundamental problem: different applications require different versions of the same library. Like, 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. It's giving messy. π
<pink>virtualenv</pink> 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. Literally the slay-est thing ever. β¨
okay so bestie, real talk: virtualenv operates in two distinct phases: [graph]
Phase 1: Discover a Python interpreter β first it has to clock which Python interpreter is gonna be the template for the whole virtual environment. By default it just uses the same Python version that virtualenv itself is running on (comfy, right?). Wanna switch it up? Hit it with the --python flag and name a different interpreter. π
Phase 2: Create the virtual environment β once the target interpreter is locked in, virtualenv builds the environment in five steps:
- Create a Python executable matching the target interpreter
- Install seed packages (
pip,setuptools,wheel) to enable package installation - Install activation scripts for various shells
- Create VCS ignore files (currently Git's
.gitignore, skip with--no-vcs-ignore) - In a project folder, point a
.venvredirect 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)
okay now the big design principle, and this one's lowkey iconic: virtual environments are not self-contained. A complete Python installation is literally thousands of files, and copying all of them into every single venv would be so wasteful π. Instead, virtual environments are lightweight little shells that borrow most of their content from the system Python. They only contain what's needed to redirect Python's behavior. it's giving minimalism, and we stan.
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, not just a description of it. That also makes it the cheapest place for other tools to learn about the environment, which is exactly 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. hybrid era, but not in the fun way π
π Python discovery
Before creating a virtual environment, virtualenv must locate a Python interpreter. That interpreter decides the virtual environment's Python version, implementation (CPython, PyPy, etc.), and architecture (32-bit or 64-bit). no pressure or anything.
The --python flag accepts several specifier formats:
- Path specifier β an absolute or relative path to a Python executable, such as
/usr/bin/python3.14or./python. - Version specifier β a string following the format
{implementation}{version}{architecture}{machine}where:- Implementation is alphabetic characters (
pythonmeans any implementation; if omitted, defaults topython). - Version is dot-separated numbers, optionally followed by
tfor free-threading builds. - Architecture is
-64or-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). cute little name-matching, we love to see it β¨
- Implementation is alphabetic characters (
Examples:
python3.14.1β Any Python implementation with version 3.14.13β Any Python implementation with major version 33.13tβ Any Python implementation version 3.13 with free-threading enabledcpython3β CPython implementation with major version 3pypy2β PyPy implementation with major version 2cpython3.12-64-arm64β CPython 3.12, 64-bit, ARM64 architecture3.11-64-x86_64β Any implementation, version 3.11, 64-bit, x86_64 architecturerustpythonβ RustPython implementation
- PEP 440 version specifier β version constraints using PEP 440 operators:
>=3.12β Any Python 3.12 or later~=3.11.0β Compatible with Python 3.11.0cpython>=3.10β CPython 3.10 or later
When you hand it a specifier, virtualenv searches for matching interpreters using this strategy: [graph]
- Windows Registry (Windows only): check registered Python installations per PEP 514 <https://www.python.org/dev/peps/pep-0514/>.
- uv-managed installations: check the
UV_PYTHON_INSTALL_DIRenvironment variable or platform-specific uv Python directories for managed Python installations. - PATH search: search for executables on the
PATHenvironment variable with names matching the specification.
π§ Version manager shim resolution
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. Here's the drama: 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. π€ not the shim gaslighting us.
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:
- The
PYENV_VERSIONenvironment variable (colon-separated for multiple versions). - A
.python-versionfile in the current directory or any parent directory. - 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. one algorithm, three besties π«Ά
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. plot twist fr fr.
π Creators
Creators are the ones responsible for constructing the virtual environment structure. virtualenv supports two types of creators:
- venv creator β this creator delegates the entire creation process to the standard library's
venvmodule, following PEP 405 <https://www.python.org/dev/peps/pep-0405/>. The venv creator requires spawning a subprocess to invoke thevenvmodule, unlessvirtualenvis installed in the system Python. The subprocess overhead can be significant, especially on Windows where process creation is expensive. πΈ - builtin creator β this creator means
virtualenvperforms the creation itself by knowing exactly which files to create and which system files to reference. The builtin creator is actually a whole family of specialized creators for different combinations of Python implementation (CPython, PyPy, GraalPy, RustPython) and platform (Windows, POSIX). The namebuiltinis 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. main character energy, no middleman π₯
[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. so thoughtful, honestly π
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. it does not care about your labels, it has a compiled-in destiny π
π± Seeders
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:
- pip seeder β this method uses the bundled
pipwheel to install seed packages by spawning a childpipprocess. 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. ninety-eight percent!! the AUDACITY π
π app-data seeder π
okay so this method is the one that 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. Every environment after that? It just links or copies those pre-built images into the virtual environment's site-packages directory. no cap. π
Performance comparison for creating virtual environments: [graph]
On platforms that support symlinks efficiently (Linux, macOS), the app-data seeder is literally giving nearly instant seeding. obsessed.
You can override the cache location using the VIRTUALENV_OVERRIDE_APP_DATA environment variable.
π Wheel acquisition π
Both seeding methods require wheel files for the seed packages. virtualenv acquires wheels using a priority system: [graph].
β Embedded wheels β
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.
π Upgraded embedded wheels π
You 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 then use these upgraded wheels instead of the embedded ones.
virtualenv can also perform periodic automatic upgrades (see below π).
π¦ Extra search directories π¦
You can specify additional directories containing wheels using the --extra-search-dir flag. This is useful in air-gapped environments or when you're using custom package builds.
πΈ PyPI download πΈ
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.
π Periodic update mechanism π
To keep the seed packages reasonably current without making you 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. fr fr.
You can disable the periodic update mechanism with the --no-periodic-update flag.
π Distribution maintainer patching π
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.
β¨ Activators β¨
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, iykyk.
What activation does: [graph]
π PATH modification π
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.
π Environment variables π
Activation sets several environment variables:
VIRTUAL_ENVβ absolute path to the virtual environment directory.VIRTUAL_ENV_PROMPTβ the prompt prefix (the environment name or a custom value from--prompt).PKG_CONFIG_PATHβ modified to include the virtual environment'slib/pkgconfigdirectory.
π Prompt modification π
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 it with the --prompt flag when creating the environment, or disable it entirely by setting the VIRTUAL_ENV_DISABLE_PROMPT environment variable.
π§ Deactivation π§
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:
- Bash β
activateβ https://www.gnu.org/software/bash/ - Fish β
activate.fishβ https://fishshell.com/ - Csh/Tcsh β
activate.cshβ https://www.tcsh.org/ - PowerShell β
activate.ps1β https://learn.microsoft.com/en-us/powershell/ - Windows Batch β
activate.batβ https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd - Nushell β
activate.nuβ https://www.nushell.sh/ - Python β
activate_this.pyβ for programmatic activation from within a running Python process, see Programmatic activation
Note: On Windows 7 and later, PowerShell's default execution policy isRestricted, which prevents running theactivate.ps1script. 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.
π₯ Release integrity π₯
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.
π Trusted publishing π
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.
π§Ύ Provenance π§Ύ
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.
π Immutable releases π
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.
π§ͺ Reproducible builds π§ͺ
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.
π¦ The SBOMs π¦
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.yamlinpypa/virtualenvbuilt and signed. - With
--source-refor--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
pipandsetuptoolshave no known vulnerabilities. The SBOM lets you look them up. π‘οΈ - Anything about wheels virtualenv fetches at run time.
--download,--upgrade-embed-wheelsand the periodic update takepipandsetuptoolsfrom PyPI, outside virtualenv's attestations. π¬ - That you have the latest release. The bootstrap zipapp in particular can trail it. π
πΈ Environment discovery πΈ
okay so editors, type checkers and task runners need to find a project's environment, and they cannot just 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 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
- The first environment in a project claims the default β Later environments leave the redirect alone, so a throwaway environment next to your working one does not move your editor to it. Pass
--venv-redirectto point the redirect at the new environment. π - Outside a project folder, virtualenv writes nothing β
virtualenv fooin a folder without apyproject.tomlleaves the folder as it was.--venv-redirectwrites the redirect there anyway. π - A
.venvfolder stays β Creating.venvitself writes nothing, and where a.venvfolder or symlink exists virtualenv leaves it alone, since it already is the default. π - Other tools' redirects stay β 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 thepyvenv.cfgthere carries thevirtualenvkey. A redirect written byuv,venvor you stays, and so does one whose target is gone, since it may still resolve inside a container or on another machine. π΅οΈ - The command line wins β
--venv-redirectand--no-venv-redirectset 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 failed write keeps the environment β 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. π«Ά
π§ Why not a list π§
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. βοΈ
π§ Provisional status π§
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. π¨
π See also π
- Use
virtualenv <>β Practical guides for common virtualenv tasks. π - Command line <> β Complete CLI reference documentation. π§Ύ
π§© Plugins π§©
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. β¨
π Extension points π
virtualenv provides four extension points through entry point groups:
virtualenv.discoveryβ Python interpreter discovery plugins. These plugins locate and identify Python interpreters that will be used as the base for creating virtual environments. πvirtualenv.createβ 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. ποΈvirtualenv.seedβ Seed package installer plugins. These plugins install initial packages (likepip,setuptools,wheel) into newly created virtual environments. π±virtualenv.activateβ 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. π«
π Your first plugin π
This tutorial walks through creating a simple discovery plugin that locates Python interpreters managed by pyenv. bestie, it's giving "hello, interpreter" energy and it's honestly iconic.
π Create the package structure π
Set up a new Python package with the following structure:
virtualenv-pyenv/
βββ pyproject.toml
βββ src/
βββ virtualenv_pyenv/
βββ __init__.py
β¨ Configure the entry point β¨
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"
π Implement the plugin π
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 the plugin π οΈ
Install your plugin in development mode alongside virtualenv:
$ pip install -e virtualenv-pyenv/
π Verify the plugin π
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
π Plugin how-to guides π
This page provides task-oriented guides for creating each type of virtualenv plugin. π«Ά
π Create a discovery 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
ποΈ Create a creator plugin ποΈ
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
β¦and here's the whole shape of a creator once you actually sit down and write one, bestie β every method named exactly what virtualenv is gonna call on it:
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()
Then you register the entry point with a naming pattern that matches both the platform and the Python version β yes, both, that's the whole point of the naming π:
[virtualenv.create]
cpython3-posix = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix
cpython3-win = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows
π± Create a seeder plugin
Seeder plugins are the ones that install the initial packages into your virtual environment β the "okay bestie, here's your starter pack" step. You register them under virtualenv.seed.
Override cannot_seed to reject target interpreters your seeder doesn't support. The base returns None for every interpreter; you return a message instead, and selection rejects the seeder before the environment even gets created, surfacing your message straight to the user. So a plugin can totally serve Python versions the bundled seeders no longer ship wheels for β like a version that's already past its support window. slay, honestly π
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 that entry point:
[virtualenv.seed]
custom = your_package.seed:CustomSeeder
π Create an activator plugin
Activator plugins generate the shell activation scripts. Register them under virtualenv.activate β this is the activate-script energy, the part everyone actually touches πΈ
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 it:
[virtualenv.activate]
bash = virtualenv.activation.bash:BashActivator
fish = virtualenv.activation.fish:FishActivator
custom = your_package.activation:CustomShellActivator
π¦ Package and distribute a plugin
Use pyproject.toml to declare your entry points β this is the part where your little plugin becomes a whole package, main character energy:
[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 right next to virtualenv:
$ pip install virtualenv-custom-plugin
Or, if you're in development mode and want to live-edit your little guy:
$ pip install -e /path/to/virtualenv-custom-plugin
Then test it by actually creating a virtual environment:
$ virtualenv --discovery=custom --creator=custom-posix --seeder=custom --activators=custom test-env
π Plugin API reference
This page documents the interfaces that plugins must implement. Like, the actual contract, no vibes-only guessing.
π Discovery
Discovery plugins locate Python interpreters for creating virtual environments. They're the little scouts out there finding you a Python π§
class virtualenv.discovery.discover.Discover(options)
Parameters
options(VirtualEnvOptions)
classmethod add_parser_arguments(parser)
Parameters
parser(ArgumentParser)
Return type
None
abstractmethod run()
Return type
PythonInfo | None
property interpreter: PythonInfo | None
- The interpreter as returned by
run(), cached.
π PythonInfo
Discovery plugins return a PythonInfo object describing the located interpreter β like a little ID card for your Python πͺͺ
class virtualenv.discovery.py_info.PythonInfo
Contains information for a Python interpreter.
platform: strimplementation: strpypy_version_info: tuple[int, ...]version_info: VersionInfoarchitecture: intsysconfig_platform: str | Noneversion_nodot: str | Noneversion: stros: strfree_threaded: booldebug_build: boolprefix: str | Nonebase_prefix: str | Nonereal_prefix: str | Nonebase_exec_prefix: str | Noneexec_prefix: str | Noneexecutable: stroriginal_executable: strsystem_executable: str | Nonehas_venv: boolpath: list[str]file_system_encoding: strstdout_encoding: str | Nonesysconfig_scheme: str | Nonesysconfig_paths: dict[str, str]distutils_install: dict[str, str]
okay so we're picking right back up in the middle of the PythonInfo attribute tour and bestie there is so much here, but every single line is iconic so we're doing all of it π
sysconfigβ type isdict[str, str | None]β¨sysconfig_varsβ type isdict[str, str | int | None]πΈtcl_libβ type isstr | Noneπtk_libβ type isstr | Noneπ¦system_stdlibβ type isstrπsystem_stdlib_platformβ type isstrβmax_sizeβ type isintπ₯
π«Ά install_path(key)
Returns the relative installation path for a given installation scheme key β¨
Parameters
key(str) β the sysconfig installation scheme key (like"scripts"or"purelib").
Return type β str
π the cute little version properties
version_str(str) β the full version as amajor.minor.microstring, e.g.3.13.2πversion_release_str(str) β the release version as amajor.minorstring, e.g.3.13πΈpython_name(str) β the python executable name aspythonX.Y, e.g.python3.13π
π the "am I in a virtualenv" checks
is_old_virtualenv(bool) βTrueif this interpreter runs inside an old-style virtualenv (it hasreal_prefix) πis_venv(bool) βTrueif this interpreter runs inside a PEP 405 venv (it hasbase_prefix) π§
π sysconfig_path(key, config_var=None, sep='/')
Returns the sysconfig install path for a scheme key, and it can optionally swap in config variables for you π«Ά
Parameters
key(str) β the sysconfig path key (e.g."purelib","include").config_var(dict[str, str | int | None] | None) β replacement mapping for sysconfig variables; when it'sNoneit uses the interpreter's own values.sep(str) β the path separator to use in the result.
Return type β str
π the system-* properties
system_include(str) β the path to the system include directory for C headers π§΅system_prefix(str) β the prefix of the system Python this interpreter is based onsystem_exec_prefix(str) β the exec prefix of the system Python this interpreter is based onsystem_exe(str) β the executable of the system Python this interpreter is based on
okay so aboutsystem_exeβsystem_executablestaysNoneuntilresolve_to_system()walks the prefix chain, which means reading it forces every caller to narrow a value that discovery has already settled. And the fallback here matches exactly what resolution itself writes when a prefix links back to its own interpreter. no cap π«‘
machine(str) β the instruction set architecture (ISA), derived fromsysconfig.get_platform()π»spec(str) β a specification string identifying this interpreter, likeCPython3.13.2-64-arm64β
π¦ classmethod clear_cache(cache)
Nukes all cached interpreter information out of cache π§Ή
Parameters
cache(PyInfoCache) β the cache store to clear.
Return type β None
π satisfies(spec, *, impl_must_match)
Checks whether a given specification can be satisfied by this python interpreter instance π
Parameters
spec(PythonSpec) β the specification to check against.impl_must_match(bool) β when it'sTrue, the implementation name has to match exactly.
Return type β bool
πΈ classmethod current(cache=None)
Locates the current host interpreter information π
Parameters
cache(PyInfoCache | None) β interpreter metadata cache; when it'sNone, results are not cached.
Return type β PythonInfo
π classmethod current_system(cache=None)
Locates the current system interpreter information, resolving all the way through any virtualenv layers π§
Parameters
cache(PyInfoCache | None) β interpreter metadata cache; when it'sNone, results are not cached.
Return type β PythonInfo
β¨ to_json()
Serializes this interpreter information into a JSON string π¦
Return type β str
π to_dict()
Converts this interpreter information into a plain dictionary β and the return type is a whole journey, look at this:
dict[str, str | int | bool | None | tuple[int | str, ...] | list[str] | dict[str, str | int | None]]
that's the entire union type, bestie, we do not gatekeep π
π₯ classmethod from_exe(exe, cache=None, *, raise_on_error=True, ignore_cache=False, resolve_to_host=True, env=None)
Gets the python information for a given executable path π
Parameters
exe(str) β path to the Python executable.cache(PyInfoCache | None) β interpreter metadata cache; when it'sNone, results are not cached.raise_on_error(bool) β raise on failure instead of returningNone.ignore_cache(bool) β bypass the cache and re-query the interpreter.resolve_to_host(bool) β resolve through virtualenv layers to the system interpreter.
and yes that's env=None sitting in the signature, still going strong, still doing its thing π
more coming, don't go anywhere β
envβMapping[str, str] | None, the environment mapping; defaults toos.environ.
Return type: PythonInfo | None
πͺ classmethod from_json(payload)
Deserialize interpreter info straight out of a JSON string. Give it the blob, get a PythonInfo back. slay.
Parameters
payloadβstr, the JSON thatto_json()produced.
Return type: PythonInfo
π§ classmethod from_dict(data)
Rebuild a PythonInfo from a plain old dictionary. No JSON shenanigans, just a dict walking in and a PythonInfo walking out π
.
Parameters
dataβdict[str, str | int | bool | None | tuple[int | str, ...] | list[str] | dict[str, str | int | None]], the dictionary thatto_dict()produced.
Return type: PythonInfo
π classmethod resolve_to_system(cache, target)
Walks the virtualenv/venv prefix chains all the way down to find the underlying system interpreter. It's giving "follow the breadcrumbs home" π.
Parameters
cacheβPyInfoCache | None, the interpreter metadata cache; when it'sNonethe results are not cached. (ngl, remember that part.)targetβPythonInfo, the interpreter you want resolved.
Return type: PythonInfo
π discover_exe(cache, prefix, *, exact=True, env=None)
Hunts down a matching Python executable underneath a given prefix directory. Detective mode: activated π΅οΈββοΈ.
Parameters
cacheβPyInfoCache, the interpreter metadata cache.prefixβstr, the directory prefix to search under.exactβbool, and when it'sTrueyou're requiring an exact version match (no vibe-based approximations, bestie).envβMapping[str, str] | None, the environment mapping; defaults toos.environ.
Return type: PythonInfo
π± App data π±
The application data interface that plugins use for caching. Think of it as the group chat's shared photo album πΈ.
ποΈ class virtualenv.app_data.base.AppData
The abstract storage interface for the virtualenv application. Everything below is marked abstractmethod, so it's the blueprint, not the build.
abstractmethod close() Called before virtualenv exits. Bye bestie π.
Return type: None
abstractmethod reset() Called when the user passes in the reset app data. Fresh start energy β¨.
Return type: None
abstractmethod py_info(path) Returns a content store for cached interpreter information at the given path.
Parameters
pathβPath, the interpreter executable path.
Return type: ContentStore
Returns: a content store for the cached data
abstractmethod py_info_clear() Clears all cached interpreter information. Poof π¨.
Return type: None
property can_update: bool True if this app data store supports updating cached content.
abstractmethod embed_update_log(distribution, for_py_version) Returns a content store for the embed update log of a distribution.
Parameters
distributionβstr, the package name (e.g.pip).for_py_versionβstr, the target Python version string.
Return type: ContentStore
Returns: a content store for the update log
property house: Path The root directory of the application data store. The literal home address π .
property transient: bool True if this app data store is transient and does not persist across runs. Here for a good time, not a long time π.
abstractmethod wheel_image(for_py_version, name) Returns the path to a cached wheel image.
Parameters
for_py_versionβstr, the target Python version string.nameβstr, the package name.
Return type: Path
Returns: the path to the cached wheel
ensure_extracted(path, to_folder=None) Makes sure a path is actually available on disk, extracting it from the zipapp if that's what it takes π§.
Parameters
pathβPath, the path to ensure is available.to_folderβPath | None, optional target directory for the extraction.
Return type: Generator[Path]
Returns: yields the usable path on disk
abstractmethod extract(path, to_folder) Extracts a path out of the zipapp to a location on disk.
Parameters
pathβPath, the path to extract.to_folderβPath | None, optional target directory.
Return type: Generator[Path]
Returns: yields the extracted path
abstractmethod locked(path) Acquires an exclusive lock on the given path. Nobody else gets in, periodt π.
Parameters
pathβPath, the path to lock.
Return type: Generator[None]
π Creators π
Creator plugins build the virtual environment directory structure and install the Python interpreter. They're the main characters of env setup π.
Fr fr, that's the whole section β the next bit picks up right where this leaves off, so don't go anywhere π
β¦anyway, back into the creator guts, because there's a metaclass down here doing the lord's work π
class virtualenv.create.creator.CreatorMeta
The metaclass behind the creators β that's the vibe. β¨
class virtualenv.create.creator.Creator(options, interpreter)
Okay so this is the class: "A class that given a python Interpreter creates a virtual environment." π¦
Parameters
options(VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>)interpreter(PythonInfo)
Construct a new virtual environment creator. π
Parameters
options(VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>) β the CLI option as parsed fromadd_parser_arguments()interpreter(PythonInfo) β the interpreter to create virtual environment from
classmethod can_create(interpreter) β Determine if we can create a virtual environment. π
Parameters
interpreter(PythonInfo) β the interpreter in question
Return type: CreatorMeta | bool <https://docs.python.org/3/builtins/functions.html#bool> | None <https://docs.python.org/3/builtins/constants.html#None>
Returns: None if we can't create, any other object otherwise that will be forwarded to add_parser_arguments() π
classmethod add_parser_arguments(parser, interpreter, meta, app_data) β Add CLI arguments for the creator. β¨
Parameters
parser(ArgumentParser <https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser>) β the CLI parserapp_data(AppData) β the application data folderinterpreter(PythonInfo) β the interpreter we're asked to create virtual environment formeta(CreatorMeta) β value as returned bycan_create()
Return type: None <https://docs.python.org/3/builtins/constants.html#None>
abstractmethod create() β Perform the virtual environment creation. Do the thing, bestie. πΈ
Return type: None <https://docs.python.org/3/builtins/constants.html#None>
add_cachedir_tag() β Generate a file indicating that this is not meant to be backed up. π«πΎ
Return type: None <https://docs.python.org/3/builtins/constants.html#None>
setup_ignore_vcs() β Generate ignore instructions for version control systems. git, you're on notice π«Ά
Return type: None <https://docs.python.org/3/builtins/constants.html#None>
point_venv_redirect() β Point the .venv redirect file of the parent folder at the environment, per PEP-832 (provisional). β
Noted as provisional in the original, so treat that behaviour as still settling β periodt.
Return type: None <https://docs.python.org/3/builtins/constants.html#None>
π± Seeders
Seeder plugins install initial packages (like pip, setuptools, wheel) into the virtual environment. That's the "okay you exist now, here's your starter pack" stage π
class virtualenv.seed.seeder.Seeder(options, enabled)
A seeder will install some seed packages into a virtual environment. π
Parameters
options(VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>)enabled(bool <https://docs.python.org/3/builtins/functions.html#bool>)
Create.
Parameters
options(VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>) β the parsed options as defined withinadd_parser_arguments()enabled(bool <https://docs.python.org/3/builtins/functions.html#bool>) β a flag whether the seeder is enabled or not
classmethod cannot_seed(interpreter) β Explain why this seeder cannot install seed packages for the given interpreter. A very polite rejection letter π
Parameters
interpreter(PythonInfo) β the interpreter the environment is based on
Return type: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>
Returns: 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 π₯
classmethod add_parser_arguments(parser, interpreter, app_data) β Add CLI arguments for this seed mechanisms.
Parameters
parser(ArgumentParser <https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser>) β the CLI parserapp_data(AppData) β the CLI parser (yes, fr fr, that's literally what the original says for this one π)interpreter(PythonInfo) β the interpreter this virtual environment is based of
Return type: None <https://docs.python.org/3/builtins/constants.html#None>
abstractmethod run(creator) β Perform the seed operation. β¨
Parameters
creator(Creator) β the creator (based ofvirtualenv.create.creator.Creator) we used to create this virtual environment
Return type: None <https://docs.python.org/3/builtins/constants.html#None>
π Activators
Activator plugins generate shell-specific activation scripts. The source-and-go moment, iconic behavior only π
class virtualenv.activation.activator.Activator(options)
Generates activate script for the virtual environment. π¦
Parameters
options(VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>)
Create a new activator generator.
Parameters
options(VirtualEnvOptions <#virtualenv.config.cli.parser.VirtualEnvOptions>) β the parsed options as defined withinadd_parser_arguments()
classmethod supports(interpreter) β Check if the activation script is supported in the given interpreter. vibe check on the interpreter π
Parameters
interpreter(PythonInfo) β the interpreter we need to support
Return type: bool <https://docs.python.org/3/builtins/functions.html#bool>
Returns: True if supported, False otherwise β
classmethod add_parser_arguments(parser, interpreter) β Add CLI arguments for this activation script.
Parameters
parser(ArgumentParser <https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser>) β the CLI parserinterpreter(PythonInfo) β the interpreter this virtual environment is based of
Return type: None <https://docs.python.org/3/builtins/constants.html#None>
abstractmethod generate(creator) β Generate activate script for the given creator. π
Parameters
creator(Creator) β the creator (based ofvirtualenv.create.creator.Creator) we used to create this virtual environment
Return type: list <https://docs.python.org/3/builtins/stdtypes.html#list>[Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>]
π§© Plugin architecture
This page explains how virtualenv's plugin system works internally. Pull up a chair, bestie, we're going under the hood π§β¨
π Entry points
virtualenv uses Python entry points (setuptools / importlib.metadata) to discover plugins. Each plugin registers under one of four entry point groups:
virtualenv.discoveryvirtualenv.createvirtualenv.seedvirtualenv.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. No extra setup, no cap π§’
π Plugin lifecycle
The following diagram shows how plugins are discovered and executed:
[graph]
The lifecycle follows these stages:
- virtualenv starts and discovers all entry points from the four plugin groups
- The CLI parser is built dynamically, incorporating options from all discovered plugins
- User arguments are parsed to select which discovery, creator, seeder, and activator plugins to use
- Selected plugins execute in sequence: discover β create β seed β activate
- Each stage passes its output to the next stage
A whole relay race and every leg slays πββοΈπ¨
π Extension point design
Each extension point follows a consistent pattern:
Base abstract class β Each extension point defines a base abstract class (Discover, Creator, Seeder, Activator) that specifies the interface plugins must implement. π
Built-in implementations β 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. π
Third-party plugins β External packages implement the base interface and register their own entry points under the same group. When installed, they appear alongside built-in options. π«Ά
CLI selection β Command-line flags (--discovery, --creator, --seeder, --activators) allow users to select which implementation to use. Multiple activators can be selected simultaneously β choose your fighter(s), plural, it's allowed π
β¨
π Parser integration π
Every plugin can chip in its own CLI arguments through the add_parser_arguments classmethod. Those args show up in virtualenv --help, and they're available whenever that plugin gets selected. cute, right? π
π How plugins interact π
Plugins run in a pipeline where each stage literally depends on the one before it. it's giving relay race, no cap πββοΈπ¨
β¨ Discovery β Creator β¨
The discovery plugin produces a PythonInfo object describing the source Python interpreter. That object is packed with metadata about the Python version, platform, paths, and capabilities. Then the creator plugin receives this PythonInfo and uses it to figure out how to build the virtual environment structure. π§
πΈ Creator β Seeder πΈ
The creator plugin produces a Creator object representing the brand-new virtual environment. It includes paths to the environment's bin directory, site-packages, and Python executable. The seeder plugin takes those paths and uses them to install packages. π¦
π¦ Seeder β Activator π¦
Once seeding is done, activator plugins use the Creator object to generate shell activation scripts. Those scripts reference the environment's bin directory and other paths to configure the shell environment. periodt. π
This pipeline makes sure every plugin has the info it needs from the previous stages. The PythonInfo flows from discovery to creator, and the Creator object flows from creator to both seeder and activators. the way that just works ππ
π Plugin isolation π
Plugins inside 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. main character energy, but for plugins π β¨
π Development π
π± Getting started π±
virtualenv is a volunteer maintained open source project and we welcome contributions of all forms. The sections below will help you get started with development, testing, and documentation. We're pleased that you are interested in working on virtualenv. This document is meant to get you setup to work on virtualenv and to act as a guide and reference to the development setup. If you face any issues during this process, please open an issue <https://github.com/pypa/virtualenv/issues/new?title=Trouble+with+development+environment> about it on the issue tracker. π«Ά
π οΈ Setup π οΈ
virtualenv is a command line application written in Python. To work on it, you'll need:
- Source code: available on GitHub <https://github.com/pypa/virtualenv>. You can use
gitto clone the repository:
git clone https://github.com/pypa/virtualenv
cd virtualenv
- Python interpreter: We recommend using CPython. You can use this guide <https://realpython.com/installing-python/> to set it up.
- tox <https://pypi.org/project/tox>: to automatically get the projects development dependencies and run the test suite. We recommend installing it using pipx <https://pipxproject.github.io/pipx/>.
π Running from source tree π
The easiest way to do this is to generate the development tox environment, and then invoke virtualenv from under the .tox/dev folder π«
tox -e dev
.tox/dev/bin/virtualenv # on Linux
.tox/dev/Scripts/virtualenv # on Windows
π§ͺ Running tests π§ͺ
virtualenv's tests are written using the pytest <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 runs the test suite on Python 3.15. Select another supported interpreter with its environment name, such as 3.9, 3.14 or pypy3.11. slay, pick your fighter π
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. π
ngl, that last bit is the classic "works on my machine" betrayal π
The tests under tests/tasks cover the build and CI tooling, hatch_build.py and the scripts in tasks/. Their outcome does not depend on the interpreter, so the interpreter environments skip them and one environment runs them on CPython 3.15:
tox -e tasks
π§Ό Running linters π§Ό
virtualenv uses pre-commit <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# noqacomments to suppress linter warnings - wherever possible, warnings should be fixed instead.# noqacomments are reserved for rare cases where the recommended style causes severe readability problems.
π Type checking π
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. iconic behavior π
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.15. A second environment checks compatibility with the minimum supported version:
tox -e type-3.9
Both environments validate that annotations are consistent and correct. β¨
π Annotation guidelines π
- Use
from __future__ import annotationsat the top of every module (enforced by ruff'srequired-importssetting). - Place imports that are only needed for type checking inside an
if TYPE_CHECKING:block to avoid runtime overhead. - Ruff's
ANNrules are enabled.ANN401(typing.Any) is suppressed on a case-by-case basis with inline# noqa: ANN401comments whereAnyis genuinely required (e.g. serialization, dynamic dispatch). - Prefer concrete types over
Any. UseUnion/|for nullable or multi-type parameters. - When a type error is genuinely unfixable (e.g. third-party library limitations), suppress it with an inline
# ty: ignore[rule-name]comment and a brief justification.
π Building documentation π
virtualenv's documentation is built using Sphinx <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. π¦
π Logo and branding π
Two logo files live in docs/_static and serve different roles, so both are kept on purpose:
virtualenv.pngis the official logo β the snakes wrapped around a terminal, with the wordmark. It is the site logo (light_logo/dark_logoindocs/conf.py) and the one to use wherever avirtualenvlogo is needed. πvirtualenv.svgis a simplified mark used only as the browser favicon (html_favicon), where the detailed PNG would be illegible at 16Γ16. It is not a vector copy of the PNG.
π’ Release π’
virtualenv's release schedule is tied to pip and setuptools. We bundle the latest version of these libraries so each time there's a new version of any of these, there will be a new virtualenv release shortly afterwards (we usually wait just a few days to avoid pulling in any broken releases). π
π¬ Performing a release π¬
A full release publishes to PyPI <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. π
β Version bumping β
The --version argument to tox r -e release controls the version. It defaults to auto, which inspects the docs/changelog directory: if any *.feature.rst or *.removal.rst fragments exist, the minor version is bumped, otherwise the patch version is bumped. You can also pass major, minor, or patch explicitly.
Both methods produce identical results: a release commit and tag on main. Pushing the tag triggers the Release workflow <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) π
- Go to the Pre-release workflow <https://github.com/pypa/virtualenv/actions/workflows/pre-release.yaml> on GitHub.
- Click Run workflow and select the bump type (
auto,major,minor, orpatch).
πΈ Locally πΈ
tox r -e release
Pass --version <bump> to override the default auto behavior (e.g. --version minor).
π©Ή Recovering a partial publication π©Ή
A failure after the PyPI upload can leave a published package without its GitHub release or bootstrap update. Keep the release commit and tag: users may have installed that version, and PyPI does not allow filename reuse <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. π₯
and bestie, that's not even the end of it β there's still more coming, so don't go anywhere πβ¨
ok so once everything is published, the release workflow does a whole post-drop vibe check on the wheel, the sdist and the zipapp. 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. And bestie, hear me: this job has no publishing credentials whatsoever. Also, a verification failure does not undo publication β so inspect the mismatch or the creation failure before retrying only the verification job. Do not rerun publication to repair a verification failure. Like, don't. π
π«Ά CONTRIBUTING π«Ά
Submitting pull requests
Submit pull requests against the main branch, and pls give a good description of what you're doing and why β the "why" is the whole point, iykyk. 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 <#compatibility-requirements> β that is, multiple Python versions and operating systems β so any pull request must consider and work on all these platforms. No "works on my machine" energy, bestie. π
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. Like separate story arcs, not one 400-episode season. π
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. Keep the diff clean, the reviewers will literally thank you. β¨
Code review
A maintainer reviews each pull request before it merges into main and checks that:
- the change does what its description says, and a test covers it;
- the change works on each Python version and operating system that
Compatibility <#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. Four checks, no shortcuts, that's the vibe check. π₯
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 β so yeah, they wrote down the whole "one person approved this" risk situation. π
AI-assisted contributions
You may use AI tools (code assistants, chat models, agents) to help write a contribution, under these conditions:
- You are the author. Read, understand and test everything you submit; you are responsible for it as if you had typed it yourself, including its licensing.
- Keep a human in the loop. Do not open pull requests or issues, or post review comments, that you have not read. Fully automated submissions are closed without review.
- Do not paste in code whose license is unknown or incompatible with the MIT License, whatever produced it.
Maintainers may use AI tools to help triage and review. A review comment posted under a maintainer's name has been read and endorsed by that maintainer. Main character energy only when you actually read the thing, fr fr. π¦
Dependency policy
virtualenv lists its runtime dependencies under [project.dependencies] in pyproject.toml, and its development and release tools in dependency groups and tox.toml. Adding a runtime dependency is a maintainer decision under the Licensing policy below. Not a "just add it real quick" situation. π
The project obtains and updates each kind of dependency this way:
- Installers fetch runtime dependencies from PyPI within the ranges
pyproject.tomlallows. - The daily
upgrade.yamlworkflow runstox r -e upgrade, which downloads newpipandsetuptoolswheels from PyPI intosrc/virtualenv/seed/wheels/embedand records their SHA-256 inBUNDLE_SHA256. The same workflow recompilespylock.zipapp.toml, which pins each distribution the zipapp bundles by version and hash. It leaves out releases younger than seven days and opens a pull request with the result. - Dependabot bumps the GitHub Actions, which the repository requires to be pinned to a commit SHA, every week after a seven-day cooldown.
pre-commit.cibumps the pre-commit hooks, which.pre-commit-config.yamlfreezes to commit SHAs.upgrade.yamlbumps the CI test tools every Monday.
We track dependencies through the CycloneDX SBOM in every wheel and the SBOMs attached to each release, which Release artifacts <> describes, and through Dependabot alerts, which cover the pip and GitHub Actions manifests. Receipts for days. π§Ύ
Software composition analysis findings follow these thresholds:
dependency-review.yamlfails a pull request that adds a dependency with a known advisory of any severity, in runtime or development scope. The main ruleset requires that check.- A shipped package whose license falls outside
ALLOWED_LICENSESfails the SBOM check intox r -e readmeandtox r -e zipapp, which the required β all checks 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
virtualenvdoes 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.
Licensing policy
virtualenv is distributed under the MIT License, and everything in the repository must be compatible with it:
- Contributions are accepted under the MIT License only; you must have the right to license what you submit.
- What
virtualenvships must use a permissive license fromALLOWED_LICENSESintasks/license_policy.py. That coversvirtualenvitself, every distribution the zipapp bundles, and the embeddedpipandsetuptoolswheels undersrc/virtualenv/seed/wheels/embed, which keep their own MIT licenses and ship unchanged. The packages those wheels vendor follow the terms of their wheel. The Unlicense entry coversfilelock3.19.1 and older, whichvirtualenvinstalls 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. Gatekeeping, but make it licensing. π« tasks/validate_sbom.pyenforces the policy on the SBOMs of the built wheel and zipapp, intox r -e readmeandtox r -e zipapp. An SPDX expression passes when eachORoffers an allowed license and everyANDoperand is allowed; aWITHexception leaves the verdict to the license it modifies. A package that declares its license by name alone fails the check until_CLASSIFIER_LICENSESinhatch_build.pymaps the name to an SPDX id..github/workflows/dependency-review.yamlfails a pull request that adds a dependency with a known vulnerability to any manifest, release tooling lock files included.- Adding a runtime dependency or bumping an embedded wheel is a maintainer decision; checking the license of the new version is part of that review.
hatch_build.pywrites a CycloneDX (https://cyclonedx.org/) SBOM into every wheel at.dist-info/sboms/virtualenv.cdx.json. It reads theBUNDLE_SUPPORT/BUNDLE_SHA256tables that back the embedded wheels above, so a wheel bump updates it with no extra step.tasks/cyclonedx_to_spdx.pyrenders that document as SPDX 2.3 with the standard library alone.tox r -e readmebuilds a wheel and validates its SBOM and the SPDX rendering; the release workflow runstox r -e spdxto writevirtualenv.spdx.jsonfromvirtualenv.cdx.jsonand check it withpyspdxtools. Both environments run on Python 3.15 with a Beartype prerelease that fixes its removed typing API import (https://github.com/beartype/beartype/issues/618). Ngl, that's a lot of SBOM lore and we stan it. πtasks/zipapp_sbom.pydescribes a builtvirtualenv.pyzfrom the archive itself:virtualenv, the embedded wheels, and each bundled dependency with the Python versions that load it, down to a SHA-256 per file. It reuses the helpers inhatch_build.py, writesvirtualenv.pyz.cdx.jsonnext to the zipapp and adds the same file to the archive root.tox r -e zipappbuilds the zipapp, runs the script, and hastasks/validate_sbom.pycheck that the SBOM lists every file in the archive with its hash.
Release artifacts <> describes every file, SBOM and attestation a release publishes, and Verify a virtualenv release <> shows users how to check them. Cute, informative, iconic. π
Automated testing
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. The "Details" link is your bestie when red β shows up. π
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. Rerun button supremacy. π
Code scanning policy
Static analysis runs on every pull request and every push to main:
pre-commit.cirunsruffwith every rule selected, theflake8-banditsecurity rules included, andzizmor(https://docs.zizmor.sh) on the workflows. Thetypeandtype-3.9tox environments runty.codeql.yamlβ runs CodeQL with thesecurity-extendedsuite plus the project queries under.github/codeql/querieson the Python code, and the Actions analysis on the workflows. It also runs every Tuesday. ποΈ
A finding blocks a merge at these thresholds, and yes blocks is the operative word:
- Any ruff, zizmor or ty finding fails a required check. no pressure π
- The main ruleset blocks a merge while CodeQL reports an alert of severity
error, or a security alert of severityhighorcritical. - Each other open CodeQL alert blocks the next release. The maintainer who cuts the release fixes it first, or dismisses it as a false positive or as not exploitable.
A CodeQL dismissal carries a comment that says who controls the flagged input and why it cannot cross a trust boundary that the threat model (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.
ποΈ NEWS entries ποΈ
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's 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.
So 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. π
βοΈ Contents of a NEWS entry βοΈ
The contents of this file are reStructuredText formatted text that will be used as the content of the news file entry. You do not need to reference the issue or PR numbers here, as towncrier will automatically add a reference to all of the affected issues when rendering the news file. π«Ά
In order to maintain a consistent style in the changelog.rst file, it's preferred to keep the news entry to the point, in sentence case, shorter than 120 characters and in an imperative tone β an entry should complete the sentence This change will β¦. In rare cases, where one line is not enough, use a summary line in an imperative tone followed by a blank line separating it from a description of the feature/change in one or more paragraphs, each wrapped at 120 characters. Remember that a news entry is meant for end users and should only contain details relevant to an end user. π
π§ Choosing the type of NEWS entry π§
A trivial change is anything that does not warrant an entry in the news file. Some examples are: code refactors that don't change anything as far as the public is concerned, typo fixes, white space modification, etc. To mark a PR as trivial a contributor simply needs to add a randomly named, empty file to the news/ directory with the extension of .trivial. lowkey the easiest job on the repo β¨
π Becoming a maintainer π
If you want to become an official maintainer, start by helping out. As a first step, we welcome you to triage issues on virtualenv's issue tracker. virtualenv maintainers provide triage abilities to contributors once they have been around for some time and contributed positively to the project. This is optional and highly recommended for becoming a virtualenv maintainer. Later, when you think you're ready, get in touch with one of the maintainers and they will initiate a vote among the existing maintainers. π³οΈ
Note: Upon becoming a maintainer, a person should be given access to various virtualenv-related tooling across multiple platforms. These are noted here for future reference by the maintainers: - GitHub Push Access - PyPI Publishing Access - CI Administration capabilities - ReadTheDocs Administration capabilities
π Maintainer access π
Current and backup maintainers turn on two-factor authentication for GitHub and PyPI, with a security key or passkey where the service supports one. We review access once a year and whenever a maintainer steps back. When someone steps back, we remove their GitHub push access, PyPI publishing access, CI administration and ReadTheDocs administration, and rotate every secret they could read. New commit access still goes through the maintainer vote described above. π
π΅οΈ Secrets and credentials π΅οΈ
The project stores two secrets β the client ID and the private key of the virtualenv-release GitHub App (https://github.com/apps/virtualenv-release) β 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
releaseenvironment, and out of the repository, workflow files, logs and repository-wide secrets. Secret scanning with push protection rejects pushes that contain a known token format. - Repository admins manage the environment secrets, and the App's owner manages its keys.
- Generate a new private key, store it in
RELEASE_APP_PRIVATE_KEYand delete the old key from the App settings at the yearly access review, when a maintainer steps back, and after a suspected leak, following the incident response plan (https://github.com/pypa/virtualenv/blob/main/.github/INCIDENT_RESPONSE.md#response).
π Current maintainers π
- BernΓ‘t GΓ‘bor (
https://github.com/gaborbernat) - Rahul Devikar (
https://github.com/rahuldevikar), who covers when BernΓ‘t is unavailable
π¦ Backup maintainer π¦
- Paul Moore (
https://github.com/pfmoore), who steps in if both current maintainers are away
πΈ Previous maintainers πΈ
- Ian Bicking (
https://github.com/ianb) - Donald Stufft (
https://github.com/dstufft)
πΊοΈ Roadmap πΊοΈ
This roadmap covers October 2026 to September 2027; BernΓ‘t GΓ‘bor last updated it on 2026-09-26. It lists work that follows from a published policy or waits on an outside decision, and it gives no release dates. We leave out work that an open pull request delivers without waiting on anyone else. β
β What we plan to do β
- Add CPython 3.16 and drop CPython 3.9 under the support policy <#compatibility-requirements>. CPython 3.9 reached end of life in October 2025, so the drop can happen from May 2027; it also retires the
_virtualenv.pthdistutils hook, which only 3.9 environments still receive (#3181https://github.com/pypa/virtualenv/issues/3181). - Keep releasing on the
pipandsetuptoolscadence that Release describes. - Record
python-versioninpyvenv.cfg(PEP 838https://peps.python.org/pep-0838/) once the PEP reaches Accepted status, restarting from the closed PR #3193https://github.com/pypa/virtualenv/pull/3193. PEP 832https://peps.python.org/pep-0832/support shipped in PR #3204https://github.com/pypa/virtualenv/pull/3204.
π What we do not plan to do π
- 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(#3237https://github.com/pypa/virtualenv/issues/3237). - Install, resolve or lock project dependencies. virtualenv creates and seeds the environment;
pip,uvand similar tools manage what goes into it. π«‘
π Release History π
v[UNRELEASED DRAFT] (2026-10-12)
No significant changes. π
v21.14.7 (2026-10-12)
Bugfixes - 21.14.7
- Escape the environment name in the bash activator for both of bash's
PS1passes, so a name or--promptvalue with a backslash before$(...)or backticks no longer runs as a command each time bash draws the prompt β by @gaborbernat (https://github.com/gaborbernat). (#3390https://github.com/pypa/virtualenv/issues/3390) π
v21.14.6 (2026-10-08)
Bugfixes - 21.14.6
- Fall back to the bundled seed wheel with periodic update on (the default) when the embed update log holds JSON of the wrong shape, instead of downloading the wheel or, offline, failing to create the environment β by @darrenhuai (
https://github.com/darrenhuai). (#3383https://github.com/pypa/virtualenv/issues/3383)
v21.14.5 (2026-10-02)
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). (#3376https://github.com/pypa/virtualenv/issues/3376)
v21.14.4 (2026-10-02)
Bugfixes - 21.14.4
- Fix
activaterunning commands from a virtual environment name or--promptthat holds$(...), backticks or${...}when zsh hasPROMPT_SUBSTset, and show a%in the name as typed under zsh (GHSA-5vjq-rrrf-7h2qhttps://github.com/pypa/virtualenv/security/advisories/GHSA-5vjq-rrrf-7h2q); reported by @kemrec (https://github.com/kemrec). (#3373https://github.com/pypa/virtualenv/issues/3373) π¨
v21.14.3 (2026-10-01)
Bugfixes - 21.14.3
- Honor
~=and every clause of a seed wheel'sRequires-Pythonwhen picking a wheel for the target Python, and skip a wheel whoseRequires-Pythonis not a valid specifier instead of failing β by @pasmud (https://github.com/pasmud). (#3369https://github.com/pypa/virtualenv/issues/3369) π
okay bestie, deep breath β it's changelog time π and no, we are not skipping it, because half of these entries are literally security lore and the other half explain why your wheel rebuilds identically on every machine. every version, every issue number, every handle, all of it, right here π«Ά
π v21.14.2 (2026-10-01)
π Bugfixes - 21.14.2
- Fixed
pyvenv.cfggetting an absolute path inpython-version,include-system-site-packagesand the other keys that hold no path when the working directory has an entry named like the value β like3.14for virtualenv3.14, ortruewith--system-site-packagesβ by @darrenhuai (#3366,https://github.com/pypa/virtualenv/issues/3366) - Fixed activation scripts running code from a virtual environment path whose parent directory carries a placeholder name such as
__VIRTUAL_NAME__(GHSA-8rjx-v5ww-45pp,https://github.com/pypa/virtualenv/security/advisories/GHSA-8rjx-v5ww-45pp), andactivate.fishrunning commands from a path or--promptthat holds a backslash before a single quote (GHSA-c947-3pg5-gm8q,https://github.com/pypa/virtualenv/security/advisories/GHSA-c947-3pg5-gm8q) β both reported by @Kwstubbs of GitHub Security Lab (#3367,https://github.com/pypa/virtualenv/issues/3367)
πΈ v21.14.1 (2026-09-29)
No significant changes. that's it. that's the release. iconic minimalism π
π¦ v21.14.0 (2026-09-29)
β¨ Features - 21.14.0
- Record the SPDX license id in the wheel and zipapp SBOMs for bundled packages that declare their license by name or classifier β like
distlibandpython-discoveryβ so license scanners can actually match them (#3339,https://github.com/pypa/virtualenv/issues/3339) - Added the Tidelift and thanks.dev funding links to the PyPI project URLs, right next to GitHub Sponsors β by @gaborbernat (
#3340,https://github.com/pypa/virtualenv/issues/3340)
π Bugfixes - 21.14.0
- Build the release sdist, wheel and zipapp with the build backend and SBOM tools that
tasks/release-requirements.txtpins by version and SHA-256, so a rebuild of the tag uses the same versions (#3337,https://github.com/pypa/virtualenv/issues/3337) - Use PEP 440 version ordering when selecting seed wheels, so pinned pre-releases, post-releases and local versions resolve to the wheel you actually asked for (
#3361,https://github.com/pypa/virtualenv/issues/3361)
π Improved Documentation - 21.14.0
- Apply the licensing policy to what the wheel and zipapp ship, checked against their SBOMs, and name
ALLOWED_LICENSESintasks/license_policy.pyas the list of accepted licenses (#3339,https://github.com/pypa/virtualenv/issues/3339) - Documented what a maintainer checks when reviewing a pull request and what the main ruleset requires before a merge β by @gaborbernat (
#3341,https://github.com/pypa/virtualenv/issues/3341) - Documented how the project selects, obtains and tracks its dependencies, and which dependency findings block a merge or a release β by @gaborbernat (
#3342,https://github.com/pypa/virtualenv/issues/3342) - Documented which static analysis findings block a merge or a release, and how maintainers suppress a finding β by @gaborbernat (
#3343,https://github.com/pypa/virtualenv/issues/3343) - Documented which secrets the project stores, who can read them, and when maintainers rotate them β by @gaborbernat (
#3344,https://github.com/pypa/virtualenv/issues/3344) - Updated the roadmap: PEP 832 (
https://peps.python.org/pep-0832/) support shipped, and PEP 838 (https://peps.python.org/pep-0838/) waits on the PEP β by @gaborbernat (#3345,https://github.com/pypa/virtualenv/issues/3345)
π v21.13.0 (2026-09-26)
β¨ Features - 21.13.0
- Added Changelog and Funding links to the PyPI project metadata, and replaced the
2020-202xplaceholder inLICENSEwith2020-presentso the copyright field of the wheel SBOM reads as a real range β by @gaborbernat (#3334,https://github.com/pypa/virtualenv/issues/3334)
π Bugfixes - 21.13.0
- Escape the curly single quotes
U+2018-U+201Bin the PowerShell activator's quote, so a virtual environment prompt, name or Tcl/Tk library path containing them can no longer close the string literal and run commands β by @gaborbernat (#3325,https://github.com/pypa/virtualenv/issues/3325) - Stopped the bash activator from re-expanding the prompt inside
PS1, so a virtual environment name or--promptvalue containing$(...), backticks or backslashes no longer runs as a command every single time bash draws the prompt β by @gaborbernat (#3326,https://github.com/pypa/virtualenv/issues/3326)
π Improved Documentation - 21.13.0
- Corrected the documented changelog fragment types and example to match the enforced towncrier categories (
#3323,https://github.com/pypa/virtualenv/issues/3323) - Listed the Unlicense and the SPDX BSD variants in the runtime dependency licensing policy, matching the
filelockreleases virtualenv installs on Python 3.9, and described the dependency review check that enforces the policy (#3329,https://github.com/pypa/virtualenv/issues/3329) - Showed how to check a release with
gh release verifyandgh release verify-asset, and how to rebuild the zipapp byte for byte with the build tool versions its SBOMs list. Also dropped the claims that the zipapp build does not pin the distributions it bundles and that wheels differ between build machines β by @gaborbernat (#3330,https://github.com/pypa/virtualenv/issues/3330)
π§ v21.12.1 (2026-09-24)
π Bugfixes - 21.12.1
- Limit the PEP 832 (
https://peps.python.org/pep-0832/).venvredirect to folders holding apyproject.tomland no.venvyet, sovirtualenv fooin a scratch folder β and tools like tox or nox building environments through virtualenv β no longer claim a folder's default environment β by @gaborbernat--venv-redirectwrites 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)
β v21.12.0 (2026-09-24)
β¨ Features - 21.12.0
- Write the PEP 838 (
https://peps.python.org/pep-0838/)python-versionkey intopyvenv.cfg, holding the target interpreter's feature release. The new Generated files <> page covers it alongside every other file a created environment holds β by @konstin (#3193,https://github.com/pypa/virtualenv/issues/3193) - Point a
.venvredirect file in the parent folder at the created environment, per PEP 832 (https://peps.python.org/pep-0832/), so editors and type checkers can find it β by @gaborbernat- virtualenv leaves a
.venvfolder alone, and leaves a redirect pointing at an environment it did not create alone too. - Pass
--no-venv-redirectto opt out. - This 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)
- virtualenv leaves a
π v21.11.1 (2026-09-23)
π 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 straight from it (
#3314,https://github.com/pypa/virtualenv/issues/3314)
π₯ v21.11.0 (2026-09-23)
β¨ 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
pipandsetuptoolswheels, and each bundled dependency with the Python versions that load it β down to a SHA-256 per file. The SBOM sits at the root ofvirtualenv.pyz, ships as thevirtualenv.pyz.cdx.jsonrelease 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.tomlpins by URL and sha256, so a release bundles only files matching the committed hashes (#3306,https://github.com/pypa/virtualenv/issues/3306) - Keep 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 theSOURCE_DATE_EPOCHtimestamp and fixed permissions instead of the build time (#3311,https://github.com/pypa/virtualenv/issues/3311)
π Improved Documentation - 21.11.0
- Documented the backup maintainer and the two-factor authentication and access review expectations for maintainers (
#3290,https://github.com/pypa/virtualenv/issues/3290) - Linked the Carl Meyer talk on the home page over HTTPS β by @gaborbernat (
#3300,https://github.com/pypa/virtualenv/issues/3300) - Added a roadmap to the development guide listing what the project plans to do β and not do β through September 2027 β by @gaborbernat (
#3304,https://github.com/pypa/virtualenv/issues/3304) - Documented what a release publishes and how to verify each file, covering the PyPI attestations, the zipapp and its provenance, the CycloneDX and SPDX SBOMs of the wheel, the zipapp SBOM, and rebuilding the sdist from its tag (
#3305,https://github.com/pypa/virtualenv/issues/3305)
π v21.10.0 (2026-09-22)
β¨ Features - 21.10.0
- Include versioned identities for packages vendored inside seed wheels in the embedded SBOM (
#3281,https://github.com/pypa/virtualenv/issues/3281) - Allow
filelock4.x on Python 3.10+ by relaxing the upper bound from<4to<5β by @r3wretrhy (#3285,https://github.com/pypa/virtualenv/issues/3285)
π 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)
v21.9.1 Β· 2026-09-22
Bugfixes β 21.9.1
- Bestie, the SBOM CSV parsing got fixed, along with unresolved dependency relationships, and we now omit CI run identifiers from reproducible builds so the output stays β¨reproducibleβ¨. (
#3278)
v21.9.0 Β· 2026-09-20
Features β 21.9.0 π₯
- The embedded SBOM is literally glow-up city now: it 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. The way that is thorough?? Obsessed. (
#3270)
Bugfixes β 21.9.0
- Added
metadata.timestamp(derived fromSOURCE_DATE_EPOCHfor reproducibility, iykyk) andmetadata.toolsnaming the generator to the embedded SBOM, and both fields get validated in the packaging checks that already run on every pull request. (#3269) - Fixed the embedded SBOM not conforming to the CycloneDX 1.6 schema (it's giving invalid vendor key on the generator tool entry π) and now we validate it against the schema in CI. (
#3270)
v21.8.1 Β· 2026-09-19
Bugfixes β 21.8.1
- The embedded SBOM's
serialNumberis now a deterministic UUID derived from the package name, version and bundled wheel hashes, so the same source tree produces a byte-identical SBOM π β and the SBOM's structure gets validated as part of the packaging checks that already run on every pull request. (#3268)
v21.8.0 Β· 2026-09-19
Features β 21.8.0 π
- Every published wheel now carries a CycloneDX SBOM at
.dist-info/sboms/virtualenv.cdx.json(PEP 770), declaring the bundledpipandsetuptoolswheels that generic SBOM tools cannot see on their own π. GitHub attests it against the released sdist and wheel. (#3265)
v21.7.16 Β· 2026-09-19
Bugfixes β 21.7.16
- Fixed
activate.cshrendering 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 β ngl that's such a niche little detail). (#3263)
v21.7.15 Β· 2026-09-18
Bugfixes β 21.7.15
- Restored an empty
PATHon deactivate in the bash and fish activators, and bash stops leaving the prompt inPS1when it was unset before activation. (#3259) - Fixed
activate.cshfailing to restorePATHand other saved variables on deactivate whenPATHwas already empty. (#3260)
v21.7.14 Β· 2026-09-18
Bugfixes β 21.7.14
- Fixed
activate.cshfailing with Event not found when the virtual environment path contains!. Classic. (#3256)
v21.7.13 Β· 2026-09-18
Bugfixes β 21.7.13
- Fixed
activateandactivate.fishrunning commands embedded in the virtual environment path or in the interpreter's Tcl/Tk library paths. Not the vibe π. (#3252)
v21.7.12 Β· 2026-09-18
Bugfixes β 21.7.12
- Fixed
activate.batrunning arbitrary commands from a crafted--prompt,VIRTUALENV_PROMPT, or config file value. Ma'am that is a security issue. (#3250) - Verify a downloaded seed wheel's sha256 against PyPI before seeding it into a virtual environment β skipped when a custom pip index is configured. (
#3251)
v21.7.11 Β· 2026-09-17
Bugfixes β 21.7.11
- Running
activate.batagain before deactivate no longer makes deactivate leave the environment'sPKG_CONFIG_PATH,TCL_LIBRARYandTK_LIBRARYbehind, or lose values the user had set before the first activation β by @darrenhuai. (#3245) - Write
pyvenv.cfgvalues on a single line, so a prompt carrying a line boundary can no longer inject configuration π«Ά.--prompt, theVIRTUALENV_PROMPTenvironment variable and the config file all set the prompt, andpyvenv.cfghas no escape syntax, so a newline, a carriage return, or any other boundarystr.splitlinesrecognizes, such asU+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, includinghome. (#3247)
Improved Documentation β 21.7.11
- Document the policy for AI-assisted contributions and the licensing rules for dependencies. (
#3239)
Misc β 21.7.11
#3238,#3240,#3241,#3242,#3243,#3244,#3246
v21.7.10 Β· 2026-09-15
Bugfixes β 21.7.10
- Skip blank lines,
#comments and lines without=inpyvenv.cfginstead of raisingValueErrorβ by @r3wretrhy. (#3232) deactivatein bash, fish and PowerShell unsetsPKG_CONFIG_PATHwhen activation found it unset, instead of keeping the environment'slib/pkgconfig. csh activation no longer drops aPKG_CONFIG_PATHthe user had set. Activation in batch, fish, nushell and PowerShell no longer adds a trailing separator whenPKG_CONFIG_PATHis unset, and PowerShell and nushell build the value with the host's path separators β by @darrenhuai. (#3233)- Activation in bash, csh, fish and PowerShell keeps the user's
TCL_LIBRARYandTK_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. (#3234)
v21.7.9 Β· 2026-09-09
Bugfixes β 21.7.9
- Replace dangling symlinks, including interpreter aliases, when recreating an environment. This prevents
FileExistsErrorwith--symlinksand writes outside the environment with--copiesβ by @darrenhuai. (#3229) - Ignore malformed or unreadable
virtualenv.inifiles and report the error in the log and--help. Accept a UTF-8 byte order mark, as written by PowerShell 5 and older Notepad versions β by @darrenhuai. (#3230)
v21.7.8 Β· 2026-09-01
Bugfixes β 21.7.8
- A missing source needed by both install modes now disables the builtin creator through
meta.error. The check comparedRefWhenvalues againstRefMustmembers, so creation went on to fail with aFileNotFoundErroror a dangling symlink β by @darrenhuai. (#3227)
v21.7.7 Β· 2026-08-28
Bugfixes β 21.7.7
- Bump the
python-discoveryminimum to>=1.6forPythonInfo.system_exe, which reports the system interpreter without the nullable typing ofsystem_executableβ by @gaborbernat. (#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. (
#3225)
v21.7.6 Β· 2026-08-28
Bugfixes β 21.7.6
- On Windows, virtualenv no longer copies the CPython 3.13+
venvlauncher.exeshim intoScriptsunder the shim's own name, and a host such aspython_d.exegets 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. (#3223)
v21.7.5 Β· 2026-08-25
Bugfixes β 21.7.5
- Fix the type check against
python-discovery1.5.2, whose annotations allow aNoneprefix and integersysconfig_varsvalues: config var substitution now skips a missing prefix and locating the shared libpython requires stringINSTSONAME/LIBDIRvalues. (#3211) ExePathRef.can_runnow checks the group and other execute bits instead of only the owner one, and returnsFalserather thanNonewhen a file carries none of the three β by @darrenhuai. (#3217)safe_deleteno longer passesignore_errors=Truetoshutil.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, whichset_treemarks β survived, so--reset-app-dataand--clearkept 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. (#3222)
v21.7.4 Β· 2026-08-10
Bugfixes β 21.7.4
- Upgrade embedded wheels:
setuptoolsto84.0.0from83.0.0(#3208)
v21.7.3 Β· 2026-08-08
Bugfixes β 21.7.3
ListType.split_valuesnow returns a list you can iterate more than once, and accepts bytes input instead of raisingTypeErrorβ by @darrenhuai. (#3207)
v21.7.2 Β· 2026-08-07
Bugfixes β 21.7.2
- Upgrade embedded wheels:
pipto26.2.1from26.2(#3206)
v21.7.1 Β· 2026-07-30
Bugfixes β 21.7.1
- Upgrade embedded wheels:
pipto 26.2 from 26.1.2#uβhttps://github.com/pypa/virtualenv/issues/uπ
π Improved Documentation β 21.7.1
- Document that
virtualenv.pngis the official logo andvirtualenv.svgis a simplified favicon-only mark β by@gaborbernatβhttps://github.com/gaborbernat.#3199βhttps://github.com/pypa/virtualenv/issues/3199π¨
π v21.7.0 (2026-07-21)
β¨ Features β 21.7.0
- Declare support for Python 3.15: CI now tests against the CPython 3.15 beta, including the free-threaded
3.15tbuild, and theProgramming Language :: Python :: 3.15classifier is advertised.#3192βhttps://github.com/pypa/virtualenv/issues/3192π
π¦ v21.6.1 (2026-07-10)
πΈ Bugfixes β 21.6.1
- Harden the fish activator prompt against user functions that shadow builtins. Routing
functions,printf,string,echoandsource/.throughbuiltinstops 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 ingh-140006βhttps://github.com/python/cpython/issues/140006.#3185βhttps://github.com/pypa/virtualenv/issues/3185ππ
π§ v21.6.0 (2026-07-06)
β¨ Features β 21.6.0
- Stop installing the
_virtualenv.{py,pth}distutils import hook for Python 3.10 and later, wherepip,setuptoolsand 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π
π v21.5.2 (2026-07-06)
πΈ Bugfixes β 21.5.2
- Upgrade embedded wheels:
setuptoolsto83.0.0#3180βhttps://github.com/pypa/virtualenv/issues/3180
π v21.5.1 (2026-06-16)
πΈ 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-seedand third-party seeders that ship compatible wheels still work β by@gaborbernatβhttps://github.com/gaborbernat.#3171βhttps://github.com/pypa/virtualenv/issues/3171π like honestly thank u for the clear error
β v21.5.0 (2026-06-13)
β¨ 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
--wheeland--no-wheeloptions 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
- Removed wheel of
π v21.4.3 (2026-06-11)
πΈ Bugfixes β 21.4.3
- Upgrade embedded wheels:
pipto 26.1.2 from 26.1.1#uβhttps://github.com/pypa/virtualenv/issues/u
- Resolve executable-only symlinks when recording
homeandbase-executableinpyvenv.cfg, mirroring CPython'sgetpath.realpath(python/cpython#115237), so environments created from a symlink to the interpreter binary locate the base stdlib (for examplepython-build-standalone); a fully symlinked interpreter tree is kept as-is β by@gaborbernatβhttps://github.com/gaborbernat.#3157βhttps://github.com/pypa/virtualenv/issues/3157π¦ - Stop exporting
PS1from the bash activator so child processes do not inherit shell prompt state.#3158βhttps://github.com/pypa/virtualenv/issues/3158π«Ά - Handle
CYGWIN/MSYS/MINGWpath conversions in fish activation script β byuser::LuNoX.#3160βhttps://github.com/pypa/virtualenv/issues/3160π
π₯ v21.4.2 (2026-05-31)
πΈ Bugfixes β 21.4.2
- Stop
deactivatein the bash/zsh activation script from aborting underset -ewhenhash -rfails (for example with shell hashing disabled) by appending|| true, matching CPythonvenv(gh-149701) and the existing non-deactivatecall β by@gaborbernatβhttps://github.com/gaborbernat.#3152βhttps://github.com/pypa/virtualenv/issues/3152π
π©· v21.4.1 (2026-05-28)
πΈ Bugfixes β 21.4.1
- Fix Windows debug build
venvlauncher_d.exesubstitution never triggering becauseexecutables()compared the source executable name instead of the target name, and fixAttributeErrorondebug_buildattribute for interpreter info objects missing the field β by@gaborbernatβhttps://github.com/gaborbernat.#3151βhttps://github.com/pypa/virtualenv/issues/3151π
π v21.4.0 (2026-05-28)
β¨ Features β 21.4.0
- Remove dead code targeting Python versions below the supported target range (PyPy 3.6, deprecated
importlibAPIs) and simplify the runtime import hook in_virtualenv.pyβ by@gaborbernatβhttps://github.com/gaborbernat.#3149βhttps://github.com/pypa/virtualenv/issues/3149π§Ή - Support Windows debug builds (
python_d.exe,venvlauncher_d.exe) matching CPythonvenvbehavior, remove dead__SCRIPT_DIR__replacement andhas_shimversion guard, drop unreachable Python 3.7 branch frompyvenv_launch_patch_active, and fix wheel deprecation message to say>= 3.9β by@gaborbernatβhttps://github.com/gaborbernat.#3150βhttps://github.com/pypa/virtualenv/issues/3150π
π¦ v21.3.3 (2026-05-13)
πΈ 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π
π«§ v21.3.2 (2026-05-12)
No significant changes. bestie we're chilling π
π v21.3.1 (2026-05-05)
πΈ Bugfixes β 21.3.1
- Upgrade embedded wheels:
pipto 26.1.1 from 26.1#3138βhttps://github.com/pypa/virtualenv/issues/3138
β¨ v21.3.0 (2026-04-27)
π« 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:
pipto 26.1#3132βhttps://github.com/pypa/virtualenv/issues/3132
π‘οΈ v21.2.4 (2026-04-14)
πΈ Bugfixes β 21.2.4
okay this release is literally all security hardening and it ate, no cap π₯
- 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 alongsideBUNDLE_SUPPORTbytasks/upgrade_wheels.py.#3119βhttps://github.com/pypa/virtualenv/issues/3119 - Security hardening: validate the distribution name and version specifier passed to
pip downloadwhen 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.zipappwithPath.relative_toso 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=1to restore the previous behavior on hosts with broken trust stores.#3122βhttps://github.com/pypa/virtualenv/issues/3122
bestie if your trust store is broken, that env var is your emergency snack. but like, only then. π
π v21.2.3 (2026-04-14)
No significant changes. π€
π§Έ v21.2.2 (2026-04-13)
πΈ Bugfixes β 21.2.2
- Bump
python-discoveryminimum to>=1.2.2to includenormalize_isasupport β by@rahuldevikarβhttps://github.com/rahuldevikar.#3117βhttps://github.com/pypa/virtualenv/issues/3117π¦
π v21.2.1 (2026-04-09)
πΈ Bugfixes β 21.2.1
- Upgrade embedded wheels:
setuptoolsto 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π
πΌ v21.2.0 (2026-03-09)
β¨ 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
- Pass
--without-scm-ignore-filesto subprocessvenvon Python 3.13+ so virtualenv controls.gitignorecreation, fixing flakytest_create_no_seedand--no-vcs-ignorebeing ignored in subprocess path β by@gaborbernatβhttps://github.com/gaborbernat.#3089βhttps://github.com/pypa/virtualenv/issues/3089π - Use
BASH_SOURCE[0]instead of$0in the bashactivatescript relocation fallback, fixing incorrectPATHwhen sourcing theactivatescript from a different directory β by@gaborbernatβhttps://github.com/gaborbernat.#3090βhttps://github.com/pypa/virtualenv/issues/3090π«Ά
π v21.1.0 (2026-02-27)
β¨ Features β 21.1.0
- Add comprehensive type annotations across the entire codebase and ship a PEP 561
py.typedmarker 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π main character type-annotation era fr fr
π¨ v21.0.0 (2026-02-25)
π Deprecations and Removals β 21.0.0
- The Python discovery logic has been extracted into a standalone
python-discoverypackage 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 tofrom python_discovery import PythonInfo. Backward-compatibility re-export shims are provided atvirtualenv.discovery.py_info,virtualenv.discovery.py_spec, andvirtualenv.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
lowkey a big glow-up for the packaging: the discovery brain moved out into its own apartment π but the old import paths are still crashing on your couch, unsupported and definitely not paying rent.
π°οΈ CHANGELOG
okay bestie, buckle up β this is the receipts section, every single release with every single issue number, no skips π
v20.39.1 (2026-02-25)
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)
v20.39.0 (2026-02-23)
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
--pythonspec string (e.g.cpython3.12-64-arm64) to distinguish between interpreters that share the same version and bitness but target different architectures. Usessysconfig.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)
v20.38.0 (2026-02-19)
Features - 20.38.0
- Store app data (pip/setuptools/wheel caches) under the OS cache directory (
platformdirs.user_cache_dir) instead of the data directory (platformdirs.user_data_dir). Existing app data at the old location is automatically migrated on first use. This ensures cached files that can be redownloaded are placed in the standard cache location (e.g.~/.cacheon Linux,~/Library/Cacheson macOS) where they are excluded from backups and can be cleaned by system tools β by@rahuldevikar(https://github.com/rahuldevikar). (#1884βhttps://github.com/pypa/virtualenv/issues/1884) (#1884βhttps://github.com/pypa/virtualenv/issues/1884) (yep, the original lists#1884twice, and we are nothing if not faithful π«Ά) - Add
PKG_CONFIG_PATHenvironment variable support to all activation scripts (Bash, Batch, PowerShell, Fish, C Shell, Nushell, and Python). The virtualenv'slib/pkgconfigdirectory is now automatically prepended toPKG_CONFIG_PATHon activation and restored on deactivation, enabling packages that usepkg-configduring build/install to find their configuration files β by@rahuldevikar(https://github.com/rahuldevikar). (#2637βhttps://github.com/pypa/virtualenv/issues/2637) this one is lowkey SO useful - Upgrade embedded pip to
26.0.1from25.3and setuptools to82.0.0,75.3.4from75.3.2,80.9.0β by@rahuldevikar(https://github.com/rahuldevikar). (#3027βhttps://github.com/pypa/virtualenv/issues/3027) - Replace
ty: ignorecomments with proper type narrowing using assertions and explicitNonechecks β by@rahuldevikar(https://github.com/rahuldevikar). (#3029βhttps://github.com/pypa/virtualenv/issues/3029)
Bugfixes - 20.38.0
- Exclude pywin32 DLLs (
pywintypes*.dll,pythoncom*.dll) from being copied to theScriptsdirectory during virtualenv creation on Windows. This fixes compatibility issues with pywin32, which expects its DLLs to be installed insite-packages/pywin32_system32by its own post-install script β by@rahuldevikar(https://github.com/rahuldevikar). (#2662βhttps://github.com/pypa/virtualenv/issues/2662) - Preserve symlinks in
pyvenv.cfgpaths to match venv behavior. Useos.path.abspath()instead ofos.path.realpath()to normalize paths without resolving symlinks, fixing issues with Python installations accessed via symlinked directories (common in network-mounted filesystems) β by@rahuldevikar(https://github.com/rahuldevikar). Fixes#2770(https://github.com/pypa/virtualenv/issues/2770). (#2770βhttps://github.com/pypa/virtualenv/issues/2770) - Fix Windows activation scripts to properly quote
python.exepath, preventing failures when Python is installed in a path with spaces (e.g.,C:\Program Files) and a file namedC:\Programexists on the filesystem β by@rahuldevikar(https://github.com/rahuldevikar). (#2985βhttps://github.com/pypa/virtualenv/issues/2985) - Fix
bash -u(set -o nounset) compatibility in bash activation script by using${PKG_CONFIG_PATH:-}and${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}to handle unsetPKG_CONFIG_PATHβ by@Fridayai700(https://github.com/Fridayai700). (#3044βhttps://github.com/pypa/virtualenv/issues/3044) - Gracefully handle corrupted on-disk cache and invalid JSON from Python interrogation subprocess instead of crashing with unhandled
JSONDecodeErrororKeyErrorβ by@gaborbernat(https://github.com/gaborbernat). (#3054βhttps://github.com/pypa/virtualenv/issues/3054)
v20.36.1 (2026-01-09)
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) security stuff, pay attention bestie π¨
v20.36.0 (2026-01-07)
Features - 20.36.0
- Add support for PEP 440 version specifiers in the
--pythonflag. Users can now specify Python versions using operators like>=,<=,~=, etc. For example, the changelog line is literally:
virtualenv --python=">=3.12" myenv . (:issue:`2994)
that (:issue:2994)` is verbatim from the original, we do not fix, we do not judge π
v20.35.4 (2025-10-28)
Bugfixes - 20.35.4
- Fix race condition in
_virtualenv.pywhen file is overwritten during import, preventingNameErrorwhen_DISTUTILS_PATCHis accessed β by@gracetyy(https://github.com/gracetyy). (#2969βhttps://github.com/pypa/virtualenv/issues/2969) - Upgrade embedded wheels:
- pip to
25.3from25.2(#2989βhttps://github.com/pypa/virtualenv/issues/2989)
- pip to
v20.35.3 (2025-10-10)
Bugfixes - 20.35.3
- Accept
RuntimeErrorintest_too_many_open_files, by@esafak(https://github.com/esafak) (#2935βhttps://github.com/pypa/virtualenv/issues/2935)
v20.35.2 (2025-10-10)
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)
v20.35.1 (2025-10-09)
Bugfixes - 20.35.1
- Patch
get_interpreterto handle missing cache and app_data β by@esafak(https://github.com/esafak) (#2972βhttps://github.com/pypa/virtualenv/issues/2972) - Fix backwards incompatible changes to
PythonInfoβ by@gaborbernat(https://github.com/gaborbernat). (#2975βhttps://github.com/pypa/virtualenv/issues/2975)
v20.35.0 (2025-10-08)
Features - 20.35.0
- Add
AppDataandCacheprotocols to discovery for decoupling β by@esafak(https://github.com/esafak). (#2074βhttps://github.com/pypa/virtualenv/issues/2074) - Ensure
python3.exeandpython3on Windows for Python 3 β by@esafak(https://github.com/esafak). (#2774βhttps://github.com/pypa/virtualenv/issues/2774)
Bugfixes - 20.35.0
- Replaced direct references to tcl/tk library paths with
getattrβ by@esafak(https://github.com/esafak) (#2944βhttps://github.com/pypa/virtualenv/issues/2944) - Restore absolute import of
fs_is_case_sensitiveβ by@esafak(https://github.com/esafak). (#2955βhttps://github.com/pypa/virtualenv/issues/2955)
v20.34.0 (2025-08-13)
Features - 20.34.0
- Abstract out caching in discovery β by
@esafak(https://github.com/esafak). DecoupleFileCachefrompy_info(discovery) β by@esafak(https://github.com/esafak). Remove references topy_infoinFileCacheβ by@esafak(https://github.com/esafak). Decouple discovery from creator plugins β by@esafak(https://github.com/esafak). Decouple discovery by duplicating info utils β by@esafak(https://github.com/esafak). (#2074βhttps://github.com/pypa/virtualenv/issues/2074) one issue, five commits, we love a multipack π - Add PyPy 3.11 support. Contributed by
@esafak(https://github.com/esafak). (#2932βhttps://github.com/pypa/virtualenv/issues/2932)
Bugfixes - 20.34.0
- Upgrade embedded wheel pip to
25.2from25.1.1β by@gaborbernat(https://github.com/gaborbernat). (#2333βhttps://github.com/pypa/virtualenv/issues/2333) - Accept
RuntimeErrorintest_too_many_open_files, by@esafak(https://github.com/esafak) (#2935βhttps://github.com/pypa/virtualenv/issues/2935) - Python in
PATHtakes precedence over uv-managed python. Contributed by@edgarrmondragon(https://github.com/edgarrmondragon). (#2952βhttps://github.com/pypa/virtualenv/issues/2952)
v20.33.1 (2025-08-05)
Bugfixes - 20.33.1
- Correctly unpack
_get_tcl_tk_libs()response inPythonInfo. Contributed by@esafak(https://github.com/esafak). (#2930βhttps://github.com/pypa/virtualenv/issues/2930) - Restore
py_info.pytimestamp intest_py_info_cache_invalidation_on_py_info_changeContributed by@esafak(https://github.com/esafak). (#2933βhttps://github.com/pypa/virtualenv/issues/2933)
v20.33.0 (2025-08-03)
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) "You're welcome." icon behaviour honestly π
Bugfixes - 20.33.0
- Prevent logging setup when
--helpis passed, fixing a flaky test. Contributed by@esafak(https://github.com/esafak). (#uβhttps://github.com/pypa/virtualenv/issues/u) yes the issue number really is just the letteru, iconic π - Fix cache invalidation for
PythonInfoby hashingpy_info.py. Contributed by@esafak(https://github.com/esafak). (#2467βhttps://github.com/pypa/virtualenv/issues/2467) - When no discovery plugins are found, the application would crash with a
StopIteration. This change catches theStopIterationand raises aRuntimeErrorwith a more informative message. Contributed by@esafak(https://github.com/esafak). (#2493βhttps://github.com/pypa/virtualenv/issues/2493) - Stop
--try-first-withoverriding absolute--pythonpaths. Contributed by@esafak(https://github.com/esafak). (#2659βhttps://github.com/pypa/virtualenv/issues/2659) - Force UTF-8 encoding for pip download Contributed by
@esafak(https://github.com/esafak). (#2780βhttps://github.com/pypa/virtualenv/issues/2780) - Creating a virtual environment on a filesystem without symlink-support would fail even with
--copiesMakefs_supports_symlinkperform a real symlink creation check on all platforms. Contributed by@esafak(https://github.com/esafak). (#2786βhttps://github.com/pypa/virtualenv/issues/2786) (that run-on sentence is verbatim, we're just holding it gently π«Ά) - Add a note to the user guide recommending the use of a specific Python version when creating virtual environments. Contributed by
@esafak(https://github.com/esafak). (#2808βhttps://github.com/pypa/virtualenv/issues/2808) - Fix 'Too many open files' error due to a file descriptor leak in virtualenv's locking mechanism. Contributed by
@esafak(https://github.com/esafak). (#2834βhttps://github.com/pypa/virtualenv/issues/2834) file descriptor leaks are the villain of every story ngl π
and that's only down to 20.33.0 β the scroll continues bestie, more versions dead ahead β¬οΈβ¨
...and those two shipped in the freshest drop, so let's get into it:
- Support renamed Windows venv redirector (
venvlauncher.exeandvenvwlauncher.exe) on Python 3.13 β Contributed by@esafak<https://github.com/esafak>. (#2851<https://github.com/pypa/virtualenv/issues/2851>) πͺ - Resolve Nushell activation script deprecation warnings by dynamically selecting the
--optionalflag for Nushellgetcommand on version0.106.0and newer, while retaining the deprecated-iflag for older versions to maintain compatibility. Contributed by@gaborbernat<https://github.com/gaborbernat>. (#2910<https://github.com/pypa/virtualenv/issues/2910>) π
π v20.32.0 (2025-07-20)
Features - 20.32.0 β¨
- Warn on incorrect invocation of Nushell activation script - by
@esafak<https://github.com/esafak>. (#nushell_activation<https://github.com/pypa/virtualenv/issues/nushell_activation>) - Discover uv-managed Python installations (
#2901<https://github.com/pypa/virtualenv/issues/2901>) π
Bugfixes - 20.32.0 π©Ή
- Ignore missing absolute paths for python discovery - by
@esafak<https://github.com/esafak> (#2870<https://github.com/pypa/virtualenv/issues/2870>) - Upgrade embedded setuptools to
80.9.0from80.3.1- by@gaborbernat<https://github.com/gaborbernat>. (#2900<https://github.com/pypa/virtualenv/issues/2900>)
πΈ v20.31.2 (2025-05-08)
No significant changes. literally nothing, bestie β a whole release of vibes and silence π
π¦ v20.31.1 (2025-05-05)
Bugfixes - 20.31.1
- Upgrade embedded wheels:
- pip to
25.1.1from25.1 - setuptools to
80.3.1from78.1.0(#2880<https://github.com/pypa/virtualenv/issues/2880>)
- pip to
π v20.31.0 (2025-05-05)
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
get_embed_wheel()no longer fails with aTypeError<https://docs.python.org/3/builtins/exceptions.html#TypeError> when it is called with an unknown distribution. (#2877<https://github.com/pypa/virtualenv/issues/2877>)- Fix
HelpFormattererror with Python3.14.0b1. (#2878<https://github.com/pypa/virtualenv/issues/2878>)
π v20.30.0 (2025-03-31)
Features - 20.30.0
- Add support for GraalPy <https://github.com/oracle/graalpython>. (
#2832<https://github.com/pypa/virtualenv/issues/2832>) πβ¨
Bugfixes - 20.30.0
- Upgrade embedded wheels:
- setuptools to
78.1.0from75.3.2(#2863<https://github.com/pypa/virtualenv/issues/2863>)
- setuptools to
π v20.29.3 (2025-03-06)
Bugfixes - 20.29.3
- Ignore unreadable directories in
PATH. (#2794<https://github.com/pypa/virtualenv/issues/2794>)
π§ v20.29.2 (2025-02-10)
Bugfixes - 20.29.2
- Remove old virtualenv wheel from the source distribution - by
@gaborbernat<https://github.com/gaborbernat>. (#2841<https://github.com/pypa/virtualenv/issues/2841>) - Upgrade embedded wheel pip to
25.0.1from24.3.1- by@gaborbernat<https://github.com/gaborbernat>. (#2843<https://github.com/pypa/virtualenv/issues/2843>)
β v20.29.1 (2025-01-17)
Bugfixes - 20.29.1
- Fix PyInfo cache incompatibility warnings - by
@robsdedude<https://github.com/robsdedude>. (#2827<https://github.com/pypa/virtualenv/issues/2827>)
π₯ v20.29.0 (2025-01-15)
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.0from75.6.0(#2823<https://github.com/pypa/virtualenv/issues/2823>)
- setuptools to
π«Ά v20.28.1 (2025-01-02)
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>)
π v20.28.0 (2024-11-25)
Features - 20.28.0
- Write
CACHEDIR.TAGfile on creation - by "user:neilramsay. (#2803<https://github.com/pypa/virtualenv/issues/2803>) π·οΈ
π v20.27.2 (2024-11-25)
Bugfixes - 20.27.2
- Upgrade embedded wheels:
- setuptools to
75.3.0from75.2.0(#2798<https://github.com/pypa/virtualenv/issues/2798>)
- setuptools to
- Upgrade embedded wheels:
- wheel to
0.45.0from0.44.0 - setuptools to
75.5.0(#2800<https://github.com/pypa/virtualenv/issues/2800>)
- wheel to
- no longer forcibly echo off during windows batch activation (
#2801<https://github.com/pypa/virtualenv/issues/2801>) πͺ - Upgrade embedded wheels:
- setuptools to
75.6.0from75.5.0 - wheel to
0.45.1from0.45.0(#2804<https://github.com/pypa/virtualenv/issues/2804>)
- setuptools to
π¦ v20.27.1 (2024-10-28)
Bugfixes - 20.27.1
- Upgrade embedded wheels:
- pip to
24.3.1from24.2(#2789<https://github.com/pypa/virtualenv/issues/2789>)
- pip to
πΈ v20.27.0 (2024-10-17)
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>) π bye 3.7
Bugfixes - 20.27.0
- When a
$PATHentry cannot be checked for existence, skip it instead of terminating - by@hroncok<https://github.com/hroncok>. (#2782<https://github.com/pypa/virtualenv/issues/2782>) - Upgrade embedded wheels:
- setuptools to
75.2.0from75.1.0 - Removed pip of
24.0 - Removed setuptools of
68.0.0 - Removed wheel of
0.42.0 - by
@gaborbernat<https://github.com/gaborbernat>. (#2783<https://github.com/pypa/virtualenv/issues/2783>)
- setuptools to
- 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>)
π v20.26.6 (2024-09-27)
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>) π not the girlies getting security-patched
π v20.26.5 (2024-09-17)
Bugfixes - 20.26.5
- Upgrade embedded wheels: setuptools to
75.1.0from74.1.2- by@gaborbernat<https://github.com/gaborbernat>. (#2765<https://github.com/pypa/virtualenv/issues/2765>)
π§ v20.26.4 (2024-09-07)
Bugfixes - 20.26.4
- no longer create
()output in console during activation of a virtualenv by.batfile. (#2728<https://github.com/pypa/virtualenv/issues/2728>) - Upgrade embedded wheels:
- wheel to
0.44.0from0.43.0 - pip to
24.2from24.1 - setuptools to
74.1.2from70.1.0(#2760<https://github.com/pypa/virtualenv/issues/2760>)
- wheel to
β v20.26.3 (2024-06-21)
Bugfixes - 20.26.3
- Upgrade embedded wheels:
- setuptools to
70.1.0from69.5.1 - pip to
24.1from24.0(#2741<https://github.com/pypa/virtualenv/issues/2741>)
- setuptools to
π₯ v20.26.2 (2024-05-13)
Bugfixes - 20.26.2
virtualenv.pyzno longer fails when zipapp path contains a symlink - by@HandSonic<https://github.com/HandSonic> and@petamas<https://github.com/petamas>. (#1949<https://github.com/pypa/virtualenv/issues/1949>)- Fix bad return code from
activate.shif hashing is disabled - by:user:'fenkes-ibm'. (#2717<https://github.com/pypa/virtualenv/issues/2717>)
π«Ά v20.26.1 (2024-04-29)
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>) πͺ
π v20.26.0 (2024-04-23)
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>) πβ¨
π v20.25.3 (2024-04-17)
Bugfixes - 20.25.3
- Python
3.13.0a6renamed pathmod to parser. (#2702<https://github.com/pypa/virtualenv/issues/2702>)
π¦ v20.25.2 (2024-04-16)
Bugfixes - 20.25.2
- Upgrade embedded wheels:
- setuptools of
69.1.0to69.5.1 - wheel of
0.42.0to0.43.0(#2699<https://github.com/pypa/virtualenv/issues/2699>)
- setuptools of
πΈ v20.25.1 (2024-02-21)
Bugfixes - 20.25.1
- Upgrade embedded wheels:
- setuptools to
69.0.3from69.0.2 - pip to
23.3.2from23.3.1(#2681<https://github.com/pypa/virtualenv/issues/2681>)
- setuptools to
- Upgrade embedded wheels:
- pip
23.3.2to24.0, - setuptools
69.0.3to69.1.0. (#2691<https://github.com/pypa/virtualenv/issues/2691>)
- pip
Misc - 20.25.1
π v20.25.0 (2023-12-01)
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.3from0.41.2(#2665<https://github.com/pypa/virtualenv/issues/2665>)
- wheel to
- Upgrade embedded wheels:
- wheel to
0.42.0from0.41.3 - setuptools to
69.0.2from68.2.2(#2669<https://github.com/pypa/virtualenv/issues/2669>)
- wheel to
π v20.24.6 (2023-10-23)
Bugfixes - 20.24.6
- Use
get_hookimplsmethod instead of the private attribute in tests. (#2649<https://github.com/pypa/virtualenv/issues/2649>) - Upgrade embedded wheels:
- setuptools to
68.2.2from68.2.0 - pip to
23.3.1from23.2.1(#2656<https://github.com/pypa/virtualenv/issues/2656>)
- setuptools to
π v20.24.5 (2023-09-08)
Bugfixes - 20.24.5
- Declare PyPy
3.10support - by@cclauss<https://github.com/cclauss>. (#2638<https://github.com/pypa/virtualenv/issues/2638>) - Brew on macOS no longer allows copy builds - disallow choosing this by
@gaborbernat<https://github.com/gaborbernat>. (#2640<https://github.com/pypa/virtualenv/issues/2640>) π - Upgrade embedded wheels:
- setuptools to
68.2.0from68.1.2(#2642<https://github.com/pypa/virtualenv/issues/2642>)
- setuptools to
π§ v20.24.4 (2023-08-30)
Bugfixes - 20.24.4
- Upgrade embedded wheels:
- setuptools to
68.1.2from68.1.0on3.8+ - wheel to
0.41.2from0.41.1on3.7+(#2628<https://github.com/pypa/virtualenv/issues/2628>)
- setuptools to
β v20.24.3 (2023-08-11)
Bugfixes - 20.24.3
- Fixed
ResourceWarningon exit caused by periodic update subprocess (#2472<https://github.com/pypa/virtualenv/issues/2472>) - Upgrade embedded wheels:
- wheel to
0.41.1from0.41.0(#2622<https://github.com/pypa/virtualenv/issues/2622>)
- wheel to
Misc - 20.24.3
π₯ v20.24.2 (2023-07-24)
Bugfixes - 20.24.2
- Upgrade embedded wheels:
- pip to
23.2.1from23.2 - wheel to
0.41.0from0.40.0(#2614<https://github.com/pypa/virtualenv/issues/2614>)
- pip to
π«Ά v20.24.1 (2023-07-19)
Bugfixes - 20.24.1
- Upgrade embedded wheels:
pipto 23.2 from 23.1.2 β by @arielkirkwood (#2611)
v20.24.0 (2023-07-14)
Features - 20.24.0
- Export the prompt prefix as
VIRTUAL_ENV_PROMPTwhen activating a virtual environment β by @jimporter (#2194) π
Bugfixes - 20.24.0
- Fix test suite β by @gaborbernat (
#2592) - Upgrade embedded wheels:
setuptoolsto 68.0.0 from 67.8.0 (#2607)
v20.23.1 (2023-06-16)
Bugfixes - 20.23.1
- update and simplify nushell activation script, fixes an issue on Windows resulting in consecutive command not found β by @melMass (
#2572) - Upgrade embedded wheels:
setuptoolsto 67.8.0 from 67.7.2 (#2588)
v20.23.0 (2023-04-27)
Features - 20.23.0
- Do not install
wheelandsetuptoolsseed packages for Python 3.12+. To restore the old behavior use:- for
wheeluseVIRTUALENV_WHEEL=bundleenvironment variable or--wheel=bundleCLI flag, - for
setuptoolsuseVIRTUALENV_SETUPTOOLS=bundleenvironment variable or--setuptools=bundleCLI flag.
- for
By @chrysle (#2487) π
- 3.12 support β by @gaborbernat (
#2558)
Bugfixes - 20.23.0
- Prevent
PermissionErrorwhen using venv creator on systems that deliver files without user write permission β by @kulikjak (#2543) - Upgrade
setuptoolsto 67.7.2 from 67.6.1 andpipto 23.1.2 from 23.1 β by @szleb (#2560)
v20.22.0 (2023-04-19)
Features - 20.22.0
- Drop support for creating Python <=3.6 (including 2) interpreters. Removed
pipof 20.3.4, 21.3.1;wheelof 0.37.1;setuptoolsof 59.6.0, 44.1.1, 50.3.2 β by @gaborbernat (#2548) π
v20.21.1 (2023-04-19)
Bugfixes - 20.21.1
- Add
tox.inito sdist β by @mtelka (#2511) - Move the use of 'let' in nushell to ensure compatibility with future releases of nushell, where 'let' no longer assumes that its initializer is a full expressions. (
#2527) - The nushell command 'str collect' has been superseded by the 'str join' command. The
activate.nuscript has been updated to reflect this change. (#2532) - Upgrade embedded wheels:
wheelto 0.40.0 from 0.38.4setuptoolsto 67.6.1 from 67.4.0pipto 23.1 from 23.0.1 (#2546)
v20.21.0 (2023-03-12)
Features - 20.21.0
- Make closure syntax explicitly starts with
{||. (#2512)
Bugfixes - 20.21.0
- Add
printcommand to nushellprint_promptto ensure compatibility with future release of nushell, where intermediate commands no longer print their result to stdout. (#2514) - Do not assume the default encoding. (
#2515) - Make
ReentrantFileLockthread-safe and, thereby, fix race condition invirtualenv.cli_runβ by @radoering (#2516) π₯
v20.20.0 (2023-02-28)
Features - 20.20.0
- Change environment variable existence check in Nushell activation script to not use deprecated command. (
#2506)
Bugfixes - 20.20.0
- Discover CPython implementations distributed on Windows by any organization β by @faph (
#2504) - Upgrade embedded
setuptoolsto 67.4.0 from 67.1.0 andpipto 23.0.1 from 23.0 β by @gaborbernat (#2510)
v20.19.0 (2023-02-07)
Features - 20.19.0
- Allow
platformdirsversion 3 β by @cdce8p (#2499)
v20.18.0 (2023-02-06)
Features - 20.18.0
- Drop 3.6 runtime support (can still create 2.7+) β by @gaborbernat (
#2489)
Bugfixes - 20.18.0
- Fix broken prompt in Nushell when activating virtual environment β by @kubouc (
#2481) - Bump embedded
pipto 23.0 andsetuptoolsto 67.1 β by @gaborbernat (#2489)
v20.17.1 (2022-12-05)
Bugfixes - 20.17.1
- A
pyorpythonspec means any Python rather than CPython β by @gaborbernat (#2460) β¨ - Make
activate.nurespectVIRTUAL_ENV_DISABLE_PROMPTand not set the prompt if requested β by @m-lima (#2461)
v20.17.0 (2022-11-27)
Features - 20.17.0
- Change Nushell activation script to be a module meant to be activated as an overlay. (
#2422) - Update operator used in Nushell activation script to be compatible with future versions. (
#2450)
Bugfixes - 20.17.0
- Do not use deprecated API from
importlib.resourceson Python 3.10 or later β by @gaborbernat (#2448) - Upgrade embedded
setuptoolsto 65.6.3 from 65.5.1 β by @gaborbernat (#2451)
v20.16.7 (2022-11-12)
Bugfixes - 20.16.7
- Use parent directory of python executable for
pyvenv.cfg"home" value per PEP 405 β by @vfazio (#2440) - In POSIX virtual environments, try alternate binary names if
sys._base_executabledoes not exist β by @vfazio (#2442) - Upgrade embedded
wheelto 0.38.4 andpipto 22.3.1 from 22.3 andsetuptoolsto 65.5.1 from 65.5.0 β by @gaborbernat (#2443)
v20.16.6 (2022-10-25)
Features - 20.16.6
- Drop unneeded shims for PyPy3 directory structure (
#2426)
Bugfixes - 20.16.6
- Fix selected scheme on debian derivatives for python 3.10 when
python3-distutilsis not installed or the venv scheme is not available β by @asottile (#2350) - Allow the test suite to pass even with the original C shell (rather than
tcsh) β by @kulikjak (#2418) - Fix fallback handling of downloading wheels for bundled packages β by @schaap (
#2429) - Upgrade embedded
setuptoolsto 65.5.0 from 65.3.0 andpipto 22.3 from 22.2.2 β by @gaborbernat (#2434)
v20.16.5 (2022-09-07)
Bugfixes - 20.16.5
- Do not turn echo off for subsequent commands in batch activators (
activate.batanddeactivate.bat) β by @pawelszramowski (#2411)
v20.16.4 (2022-08-29)
Bugfixes - 20.16.4
- Bump embed
setuptoolsto 65.3 β by @gaborbernat (#2405)
v20.16.3 (2022-08-04)
Bugfixes - 20.16.3
- Upgrade embedded
pipto 22.2.2 from 22.2.1 andsetuptoolsto 63.4.1 from 63.2.0 β by @gaborbernat (#2395)
v20.16.2 (2022-07-27)
Bugfixes - 20.16.2
- Bump embedded
pipfrom 22.2 to 22.2.1 β by @gaborbernat (#2391)
v20.16.1 (2022-07-26)
Features - 20.16.1
- Update Nushell activation scripts to version 0.67 β by @kubouch (
#2386) π¦
v20.16.0 (2022-07-25)
Features - 20.16.0
- Drop support for running under Python 2 (still can generate Python 2 environments) β by @gaborbernat (
#2382) - Upgrade embedded
pipto 22.2 from 22.1.2 andsetuptoolsto 63.2.0 from 62.6.0 β by @gaborbernat (#2383)
v20.15.1 (2022-06-28)
Bugfixes - 20.15.1
- Fix the incorrect operation when
setuptoolsplugins output something intostdout. (#2335) CPython3Windowscreator ignores missingDLLsdir. (#2368)
v20.15.0 (2022-06-25)
Features - 20.15.0
- Support for Windows embeddable Python package: includes
python<VERSION>.zipin the creator sources β by @reksarka (#1774) πΈ
Bugfixes - 20.15.0
- Upgrade embedded
setuptoolsto 62.3.3 from 62.6.0 andpipto 22.1.2 from 22.0.4 β by @gaborbernat (#2348) - Use
shlex.quoteinstead of deprecatedpipes.quotein Python 3 β by @frenzymadness (#2351) - Fix Windows PyPy 3.6 β by @reksarka (
#2363)
okay bestie real talk, that was a lot of version numbers and I know it π but this is the receipts section β every single bump, fix and "by @someone" is here so you can point at it later. more coming, don't go anywhere π«Ά
β¨ CHANGELOG β¨
Receipts time π
Hereβs every release, every fix, and every embedded-setuptools glow-up β no skipped entries, no cap.
v20.14.1 (2022-04-11)
Features - 20.14.1
- Support for creating a virtual environment from a Python 2.7 framework on macOS 12 β legacy girlies, this oneβs for you β by @nickhutchinson. (#2284)
Bugfixes - 20.14.1
- Embedded
setuptoolsgot a glow-up to 62.1.0 from 61.0.0 β by @gaborbernat. (#2327)
v20.14.0 (2022-03-25)
Features - 20.14.0
- Nushell activation scripts are supported with
nuversion 0.60 β by @kubouch. (#2321)
Bugfixes - 20.14.0
- Embedded
setuptoolsupgraded to 61.0.0 from 60.10.0 β by @gaborbernat. (#2322)
v20.13.4 (2022-03-18)
Bugfixes - 20.13.4
- Python startup inside created virtualenvs got a performance glow-up β by @asottile. (#2317)
- Embedded
setuptoolsupgraded to 60.10.0 from 60.9.3 β by @gaborbernat. (#2320)
v20.13.3 (2022-03-07)
Bugfixes - 20.13.3
- No more symlinking the contents of
/usrinto PyPy3.8+ virtualenvs β by @stefanor. (#2310) - Embedded
pipbumped from 22.0.3 to 22.0.4 β by @gaborbernat. (#2311)
v20.13.2 (2022-02-24)
Bugfixes - 20.13.2
- Embedded
setuptoolsupgraded to 60.9.3 from 60.6.0 β by @gaborbernat. (#2306)
v20.13.1 (2022-02-05)
Bugfixes - 20.13.1
- Fixed the
"execv() arg 2 must contain only strings"error on M1 MacOS (#2282) - Embedded
setuptoolsupgraded to 60.5.0 from 60.2.0 β by @asottile. (#2289) - Embedded
pipupgraded to 22.0.3 andsetuptoolsto 60.6.0 β by @gaborbernat and @asottile. (#2294)
v20.13.0 (2022-01-02)
Features - 20.13.0
- Downloaded wheel information now goes into the relevant JSON embed file so the same wheel doesnβt get downloaded again β by @mayeut. (#2268)
Bugfixes - 20.13.0
- Fixed
AttributeError: 'bool' object has no attribute 'error'when creating a Python 2.x virtualenv on macOS β by moreati. (#2269) - Fixed
PermissionError: [Errno 1] Operation not permittedwhen creating a Python 2.x virtualenv on macOS/arm64 β by moreati. (#2271)
v20.12.1 (2022-01-01)
Bugfixes - 20.12.1
- When youβre inside an update grace period, it now tries previous updates of
pip,setuptools&wheelinstead of always falling back to embedded wheels β by @mayeut. (#2265) - New patch versions of
pip,setuptools&wheelare now returned in the expected timeframe β by @mayeut. (#2266) - Manual upgrades of
pip,setuptools&wheeldonβt get discarded by a periodic update anymore β by @mayeut. (#2267)
v20.12.0 (2021-12-31)
Features - 20.12.0
- The
python2exe is now signed on Darwin arm64 β by @tmspicer. (#2233)
Bugfixes - 20.12.0
- Fixed the
--downloadoption β by @mayeut. (#2120) - Embedded
setuptoolsupgraded to 60.2.0 from 60.1.1 β by @gaborbernat. (#2263)
v20.11.2 (2021-12-29)
Bugfixes - 20.11.2
- Fixed installation of pinned versions of
pip,setuptools&wheelβ by @mayeut. (#2203)
v20.11.1 (2021-12-29)
Bugfixes - 20.11.1
- Embedded
setuptoolsbumped to 60.1.1 from 60.1.0 β by @gaborbernat. (#2258)
v20.11.0 (2021-12-28)
Features - 20.11.0
- Avoided a deprecation warning from the
py-filelockargument β by @ofek. (#2237) - Embedded
setuptoolsupgraded to 61.1.0 from 58.3.0 β by @gaborbernat. (#2240) - Dropped the runtime dependency on
backports.entry-points-selectableβ by @hroncok. (#2246) - Fish:
PATHvariables should not be quoted when being set β by @d3dave. (#2248)
v20.10.0 (2021-11-01)
Features - 20.10.0
- If a
"venv"install scheme exists insysconfig, virtualenv now uses it to create new virtual environments. This lets Python distributors like Fedora patch/replace the default install scheme without affecting the paths in new virtual environments. A similar technique was proposed to Python for thevenvmodule (seehttps://bugs.python.org/issue45413) β by hroncok (#2208) - The activated virtualenv prompt is now always wrapped in parentheses. This affects venvs created with the
--promptattribute, and matches virtualenvβs behavior on par withvenv. (#2224)
Bugfixes - 20.10.0
- Fixed the broken prompt set up by
activate.batβ by @SiggyBar. (#2225)
v20.9.0 (2021-10-23)
Features - 20.9.0
- Special-cased
--prompt .to the name of the current directory β by @rkm. (#2220) - Added
libffi-8.dllto pypy windows #2218 β by @mattip
Bugfixes - 20.9.0
- Fixed a path collision that could lead to a
PermissionErroror writing to system directories when using PyPy3.8 β by @mgorny. (#2182) - Embedded
setuptoolsupgraded to 58.3.0 from 58.1.0 andpipto 21.3.1 from 21.2.4 β by @gaborbernat. (#2205) - Removed a stray closing parenthesis in
activate.batβ by @SiggyBar. (#2221)
v20.8.1 (2021-09-24)
Bugfixes - 20.8.1
- Fixed a bug where, while creating a venv on top of an existing one without cleaning, if a seeded wheel version mismatch occurred, multiple
.dist-infodirectories could be present and confound entrypoint discovery β by @arcivanov (#2185) - Embedded
setuptoolsbumped from 58.0.4 to 58.1.0 β by @gaborbernat. (#2195)
Misc - 20.8.1
- #2189
v20.8.0 (2021-09-16)
- Embedded
setuptoolsupgraded to 58.0.4 from 57.4.0 andpipto 21.2.4 from 21.2.3 - Added the nushell activation script
v20.7.2 (2021-08-10)
Bugfixes - 20.7.2
- Embedded
pipupgraded to 21.2.3 from 21.2.2 andwheelto 0.37.0 from 0.36.2 β by @gaborbernat. (#2168)
v20.7.1 (2021-08-09)
Bugfixes - 20.7.1
- Fixed unpacking dictionary items in
PythonInfo.install_path(#2165)
v20.7.0 (2021-07-31)
Bugfixes - 20.7.0
- Embedded
pipupgraded to 21.2.2 from 21.1.3 andsetuptoolsto 57.4.0 from 57.1.0 β by @gaborbernat (#2159)
Deprecations and Removals - 20.7.0
- Removed the xonsh activator because it was breaking CI fairly often and the package maintainers werenβt supporting it; upstream is encouraged to keep supporting the project as a plugin (see
https://github.com/xonsh/xonsh/issues/3689) β by @gaborbernat. (#2160)
v20.6.0 (2021-07-14)
Features - 20.6.0
- Support for Python interpreters without
distutils(falls back tosyconfigin these cases) β by @gaborbernat. (#1910)
v20.5.0 (2021-07-13)
Features - 20.5.0
- Plugins now use
'selectable'entry points β by @jaraco. (#2093) - Added
libffi-7.dllto the hard-coded list of dlls for PyPy (#2141) - Switched from
appdirsto the better maintainedplatformdirsβ by @gaborbernat. (#2142)
Bugfixes - 20.5.0
- Bumped the embedded
pipto 21.1.3 andsetuptoolsto 57.1.0 β by @gaborbernat. (#2135)
Deprecations and Removals - 20.5.0
- Dropped Python 3.4 support since itβs been over 2 years since EOL β by @gaborbernat. (#2141)
v20.4.7 (2021-05-24)
Bugfixes - 20.4.7
- Embedded
pipupgraded to 21.1.2 andsetuptoolsto 57.0.0 β by @gaborbernat. (#2123)
v20.4.6 (2021-05-05)
Bugfixes - 20.4.6
- Fixed
site.getsitepackages()being broken on python2 on debian β by @freundTech. (#2105)
v20.4.5 (2021-05-05)
Bugfixes - 20.4.5
pipbumped to 21.1.1 from 21.0.1 β by @gaborbernat. (#2104)- β¦aaaand the last one from that batch:
site.getsitepackages()was ignoring--system-site-packageson python2 β fixed, ty@freundTech(https://github.com/freundTech), tracked in#2106(https://github.com/pypa/virtualenv/issues/2106) π
v20.4.4 (2021-04-20)
Bugfixes - 20.4.4 π
- The built-in discovery class is always preferred over plugin-supplied classes β no cap, builtin stays the main character.
#2087(https://github.com/pypa/virtualenv/issues/2087) - Upgraded the embedded setuptools to 56.0.0, by
@gaborbernat(https://github.com/gaborbernat).#2094(https://github.com/pypa/virtualenv/issues/2094)
v20.4.3 (2021-03-16)
Bugfixes - 20.4.3 π
- Bumped embedded setuptools from 52.0.0 to 54.1.2 β by
@gaborbernat(https://github.com/gaborbernat),#2069(https://github.com/pypa/virtualenv/issues/2069) - Fixed PyPy3 stdlib being incorrect on Windows β by
@gaborbernat(https://github.com/gaborbernat),#2071(https://github.com/pypa/virtualenv/issues/2071)
v20.4.2 (2021-02-01)
Bugfixes - 20.4.2 π
- Running
virtualenv --upgrade-embed-wheelswas crashing β by@gaborbernat(https://github.com/gaborbernat),#2058(https://github.com/pypa/virtualenv/issues/2058)
v20.4.1 (2021-01-31)
Bugfixes - 20.4.1 π
- Bumped the embedded pip and setuptools packages to the latest upstream supported versions (21.0.1 and 52.0.0) β by
@gaborbernat(https://github.com/gaborbernat),#2060(https://github.com/pypa/virtualenv/issues/2060)
v20.4.0 (2021-01-19)
Features - 20.4.0 β¨
- On the programmatic API you can now pass in the environment variable dictionary to use β it defaults to
os.environif you don't specify one β by@gaborbernat(https://github.com/gaborbernat),#2054(https://github.com/pypa/virtualenv/issues/2054)
Bugfixes - 20.4.0 π
- Upgraded the embedded setuptools to 51.3.3 from 51.1.2 β by
@gaborbernat(https://github.com/gaborbernat),#2055(https://github.com/pypa/virtualenv/issues/2055)
v20.3.1 (2021-01-13)
Bugfixes - 20.3.1 π
- Bumped embed pip to 20.3.3, setuptools to 51.1.1 and wheel to 0.36.2 β by
@gaborbernat(https://github.com/gaborbernat),#2036(https://github.com/pypa/virtualenv/issues/2036) - Let the unfunctioning of
pydocfail freely, so virtualenvs can be activated under Zsh withset -e(otherwiseunset -fandunfunctionexit with 1 when the function doesn't exist in Zsh) β by@d125q(https://github.com/d125q),#2049(https://github.com/pypa/virtualenv/issues/2049) - Dropped cached python information when the system executable is no longer present (like when the executable is a shim and the mapped executable got replaced β exactly the pyenv situation) β by
@gaborbernat(https://github.com/gaborbernat),#2050(https://github.com/pypa/virtualenv/issues/2050)
v20.3.0 (2021-01-10)
Features - 20.3.0 β¨
- The builtin discovery now takes a
--try-first-withargument and it's attempted first as a valid interpreter π You can use it to force discovery of a given python executable when the discovery order/mechanism starts raising errors β by@gaborbernat(https://github.com/gaborbernat),#2046(https://github.com/pypa/virtualenv/issues/2046)
Bugfixes - 20.3.0 π
- On Windows python 3.7+ distributions where the exe shim is missing, it falls back to the old ways β by
@gaborbernat(https://github.com/gaborbernat),#1986(https://github.com/pypa/virtualenv/issues/1986) - When discovering interpreters on Windows via PEP-514, PythonCore releases are preferred over the others. virtualenv gets used via pip mostly by this distribution, so it wins over stuff like conda β by
@gaborbernat(https://github.com/gaborbernat),#2046(https://github.com/pypa/virtualenv/issues/2046)
v20.2.2 (2020-12-07)
Bugfixes - 20.2.2 π
- Bumped 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)
v20.2.1 (2020-11-23)
- No significant changes. (a quiet era, she was resting π )
v20.2.0 (2020-11-21)
Features - 20.2.0 β¨
- Optionally skip the VCS ignore directive for the entire virtualenv directory, using the option
no-vcs-ignore, which isFalseby default β#2003(https://github.com/pypa/virtualenv/issues/2003) - Added the
--read-only-app-dataoption so creation can happen on top of an existing app data cache that isn't writable. Lowkey iconic for stuff like producing 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 π
- Fixed the processing of the
VIRTUALENV_PYTHONenvironment variable and made it multi-value too (separated by comma) β by@pneff(https://github.com/pneff),#1998(https://github.com/pypa/virtualenv/issues/1998)
v20.1.0 (2020-10-25)
Features - 20.1.0 β¨
- The python specification can now take one or more values, and the first one found is used to create the virtual environment β by
@gaborbernat(https://github.com/gaborbernat),#1995(https://github.com/pypa/virtualenv/issues/1995)
v20.0.35 (2020-10-15)
Bugfixes - 20.0.35 π
- Bumped embedded setuptools from 50.3.0 to 50.3.1 β by
@gaborbernat(https://github.com/gaborbernat),#1982(https://github.com/pypa/virtualenv/issues/1982) - After importing virtualenv, passing
cwdto a subprocess call broke with an invalid directory β by@gaborbernat(https://github.com/gaborbernat),#1983(https://github.com/pypa/virtualenv/issues/1983)
v20.0.34 (2020-10-12)
Bugfixes - 20.0.34 π
- Aligned with the
venvmodule when creating virtual environments with the builtin creator on Windows 3.7 and later β by@gaborbernat(https://github.com/gaborbernat),#1782(https://github.com/pypa/virtualenv/issues/1782) - Handled Cygwin path conversion in the activation script β by
@davidcoghlan(https://github.com/davidcoghlan),#1969(https://github.com/pypa/virtualenv/issues/1969)
v20.0.33 (2020-10-04)
Bugfixes - 20.0.33 π
- Fixed a
Nonetype error in cygwin when there's a POSIX path indestβ by@danyeaw(https://github.com/danyeaw),#1962(https://github.com/pypa/virtualenv/issues/1962) - Fixed Python 3.4 incompatibilities (and added it back to the CI) β by
@gaborbernat(https://github.com/gaborbernat),#1963(https://github.com/pypa/virtualenv/issues/1963)
v20.0.32 (2020-10-01)
Bugfixes - 20.0.32 π
- For activation scripts, always use UNIX line endings (unless it's BATCH shell related) β by
@saytosid(https://github.com/saytosid),#1818(https://github.com/pypa/virtualenv/issues/1818) - Upgraded embedded pip to 20.2.1 and setuptools to 49.4.0 β by
@gaborbernat(https://github.com/gaborbernat),#1918(https://github.com/pypa/virtualenv/issues/1918) - Avoided spawning new windows when doing seed package upgrades in the background on Windows β by
@gaborbernat(https://github.com/gaborbernat),#1928(https://github.com/pypa/virtualenv/issues/1928) - Fixed a bug where reading and writing on the same file could cause a race across multiple processes β
#1938(https://github.com/pypa/virtualenv/issues/1938) - Upgraded embedded setuptools to 50.2.0 and pip to 20.2.3 β by
@gaborbernat(https://github.com/gaborbernat),#1939(https://github.com/pypa/virtualenv/issues/1939) - Provided the correct path for the bash activator in cygwin or msys2 β by
@danyeaw(https://github.com/danyeaw),#1940(https://github.com/pypa/virtualenv/issues/1940) - Relaxed the importlib requirement to allow
version<3β by@usamasadiq(https://github.com/usamasadiq),#1953(https://github.com/pypa/virtualenv/issues/1953) pthfiles were not processed on CPython2 when$PYTHONPATHwas pointing tosite-packages/β by@navytux(https://github.com/navytux),#1959(https://github.com/pypa/virtualenv/issues/1959) and#1960(https://github.com/pypa/virtualenv/issues/1960)
v20.0.31 (2020-08-17)
Bugfixes - 20.0.31 π
- Upgraded 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)
v20.0.30 (2020-08-04)
Bugfixes - 20.0.30 π
- Upgraded 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)
v20.0.29 (2020-07-31)
Bugfixes - 20.0.29 π
- Upgraded embedded pip from version 20.1.2 to 20.2 β by
@gaborbernat(https://github.com/gaborbernat),#1909(https://github.com/pypa/virtualenv/issues/1909)
v20.0.28 (2020-07-24)
Bugfixes - 20.0.28 π
- Fixed the test suite failing when run from system Python β by
@gaborbernat(https://github.com/gaborbernat),#1882(https://github.com/pypa/virtualenv/issues/1882) - Provided the
setup_loggingflag to the python API so users can bypass logging handling if their application already does that itself β by@gaborbernat(https://github.com/gaborbernat),#1896(https://github.com/pypa/virtualenv/issues/1896) - Used
\nrather than\r\nas the line separator for the report (because Python already performs that transformation automatically when writing to the logging pipe) β by@gaborbernat(https://github.com/gaborbernat),#1905(https://github.com/pypa/virtualenv/issues/1905)
v20.0.27 (2020-07-15)
Bugfixes - 20.0.27 π
- No longer preimports
threading, so support for gpython (https://pypi.org/project/pygolang/#gpython) and gevent (https://www.gevent.org/) is fixed β by@navytux(https://github.com/navytux),#1897(https://github.com/pypa/virtualenv/issues/1897) - Upgraded setuptools from 49.2.0 on Python 3.5+ β by
@gaborbernat(https://github.com/gaborbernat),#1898(https://github.com/pypa/virtualenv/issues/1898)
v20.0.26 (2020-07-07)
Bugfixes - 20.0.26 π
- Bumped the dependency to
distutils >= 0.3.1β by@gaborbernat(https://github.com/gaborbernat),#1880(https://github.com/pypa/virtualenv/issues/1880) - Improved periodic update handling:
- better logging output while running, plus logging enabled on the background process call (
_VIRTUALENV_PERIODIC_UPDATE_INLINEmay be used to debug behavior inline) - fallback to an unverified context when querying PyPi for the release date,
- stop downloading wheels once we reach the embedded version,
- better logging output while running, plus logging enabled on the background process call (
by @gaborbernat (https://github.com/gaborbernat), #1883 (https://github.com/pypa/virtualenv/issues/1883)
more where that came from, keep scrolling bestie πβ¨
(picking up mid-list, bestie π )
- if the app exits with
SystemExit(0), we do not print an error message about it anymore β by@gaborbernat. (#1885) - embedded setuptools got bumped from
47.3.1to49.1.0for Python 3.5+ β by@gaborbernat. (#1887)
π v20.0.24 (2020-06-22)
Features - 20.0.24
- ok so the seeded packages are not allowed to get too crusty, and the fix is two-fold:
- there's a CLI flag that triggers an upgrade of the embedded wheels, and it lives under
upgrade-embed-wheels - every 14 days, in a background process, the embedded wheels get upgraded β and then they get used once they've been released for more than 28 days (you can switch this off with
no-periodic-update)
- there's a CLI flag that triggers an upgrade of the embedded wheels, and it lives under
more details live under Wheel acquisition β by @gaborbernat. (#1821)
- upgrade embed wheel content, we're eating good π₯
- wheels for Python 3.9 and 3.10 are shipped now
- setuptools for Python 3.5+ goes from
47.1.1to47.3.1
by @gaborbernat. (#1841)
- the final summary output now shows the installed seed package versions, like this:
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. (#1864)
Bugfixes - 20.0.24
- do not generate/overwrite
.gitignoreif it already exists at the destination path β by@gaborbernat. (#1862) - better error message when there's no
.dist-infoinside the app-data copy seeder β by@gaborbernat. (#1867)
Improved Documentation - 20.0.24
- how seeding mechanisms discover (and automatically keep up to date) wheels, over at Wheel acquisition β by
@gaborbernat. (#1821) - how distributions should handle shipping their own embedded wheels, over at Distribution maintainer patching β by
@gaborbernat. (#1840)
πΈ v20.0.23 (2020-06-12)
Bugfixes - 20.0.23
- fixed a typo in
setup.cfgβ by@RowdyHowell. (#1857)
π¦ v20.0.22 (2020-06-12)
Bugfixes - 20.0.22
- relaxed the
importlib.resourcesrequirement so version 2 is also allowed β by@asottile. (#1846) - upgraded embedded setuptools to
44.1.1for python 2 and47.1.1for python3.5+ β by@gaborbernat. (#1855)
β¨ v20.0.21 (2020-05-20)
Features - 20.0.21
- we generate an ignore file for version control systems so virtual environments don't get tracked by default. if you do still want to track them, you should remove these files. for now we support only git β by
@gaborbernat. (#1806)
Bugfixes - 20.0.21
- virtualenv sometimes failed when run concurrently, because
--clear-app-dataconflicts with theclearflag when abbreviation is turned on. to get around that while still allowing abbreviated flags on the command line, it had to move toreset-app-dataβ by@gaborbernat. (#1824) - upgraded embedded setuptools to
46.4.0from46.1.3on Python 3.5+, and pip from20.1to20.1.1β by@gaborbernat. (#1827) - the seeder pip now correctly handles
--extra-search-dirβ by@frenzymadness. (#1834)
π v20.0.20 (2020-05-04)
Bugfixes - 20.0.20
- fixed download failing with python 3.4 β by
@gaborbernat. (#1809) - older CPython2 versions use
_get_makefile_filenameinstead ofget_makefile_filenameonsysconfig, and that's handled now β by@ianw. (#1810) - fixed download being
Trueby default β by@gaborbernat. (#1813) - the app-data seed operation now fails when a wheel download fails, plus a better error message β by
@gaborbernat. (#1814)
π v20.0.19 (2020-05-03)
Bugfixes - 20.0.19
- fixed generating a Python 2 environment from Python 3 creating an invalid python activator β by
@gaborbernat. (#1776) - fixed pinning seed packages via the app-data seeder raising Invalid Requirement β by
@gaborbernat. (#1779) - interpreter discovery no longer stops when we fail to find the system interpreter for an executable during discovery β by
@gaborbernat. (#1781) - on CPython2 POSIX platforms,
syconfig.get_makefile_filenameis now ensured to exist within the virtual environment (some c-extension based libraries β e.g. numpy β use it for building) β by@gaborbernat. (#1783) - better handling of the
copiesandsymlinksoptions. there's now a priority for where the option is set, in this order: CLI, env var, file, hardcoded. if both are set at the same level, copy wins over symlink β by@gaborbernat. (#1784) - upgraded pip for Python 2.7 and 3.5+ from
20.0.2to20.1β by@gaborbernat. (#1793) - fixed CPython not being discovered from the Windows registry, and pythons are now discovered from the Windows registry in decreasing order by version β by
@gaborbernat. (#1796) - fixed symlink detection for creators β by
@asottile. (#1803)
π v20.0.18 (2020-04-16)
Bugfixes - 20.0.18
- importing setuptools before
cli_runcould make our python information query fail, because setuptools patchesdistutils.dist.Distributionβ by@gaborbernat. (#1771)
π v20.0.17 (2020-04-09)
Features - 20.0.17
- the environment variables checked for configuration now also check aliases (so setting either
VIRTUALENV_COPIESorVIRTUALENV_ALWAYS_COPYwill work) β by@gaborbernat. (#1763)
π§ v20.0.16 (2020-04-04)
Bugfixes - 20.0.16
- seed wheel files inside the
extra-search-dirfolders are now allowed to have noRequires-Pythonmetadata specified β those are considered compatible with all python versions β by@gaborbernat. (#1757)
β v20.0.15 (2020-03-27)
Features - 20.0.15
- upgraded embedded setuptools to
46.1.3from46.1.1β by@gaborbernat. (#1752)
π«Ά v20.0.14 (2020-03-25)
Features - 20.0.14
__PYVENV_LAUNCHER__gets removed on macOs for Python 3.7.(<8) and 3.8.(<3) at interpreter startup via a pth file β this pulls in the upstream patchhttps://github.com/python/cpython/pull/9516β by@gaborbernat. (#1704)- upgraded embedded setuptools for Python 3.5+ to
46.1.1, and for Python 2.7 to44.1.0β by@gaborbernat. (#1745)
Bugfixes - 20.0.14
- fixed discovery of an interpreter by name from
PATHthat does not match a spec format β by@gaborbernat. (#1746)
π©Ή v20.0.13 (2020-03-19)
Bugfixes - 20.0.13
- no more failing when the pyc files is missing for the host Python 2 β by
@gaborbernat. (#1738) - support broken Packaging pythons that put the include headers under the distutils pattern rather than the sysconfig one β by
@gaborbernat. (#1739)
π« v20.0.12 (2020-03-19)
Bugfixes - 20.0.12
- fixed relative path discovery of interpreters β by
@gaborbernat. (#1734)
π₯ v20.0.11 (2020-03-18)
Features - 20.0.11
- improved the error message for when the host python does not satisfy the 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, if we did find creators that can describe the given Python interpreter, we'll still print no supported creator for Jython, but we print exactly what host files do not allow creation of virtual environments in the case of CPython/PyPy β by
@gaborbernat. (#1716)
π Bugfixes β 20.0.11
π quick link legend so nothing gets lost: every@handledown here is literallyhttps://github.com/handle, and every#NNNNishttps://github.com/pypa/virtualenv/issues/NNNN. The couple of oddballs get their full URLs spelled out inline. That's the whole link farm, iykyk π
- Python 3 Framework support for the build XCode ships, on macOs Catalina and before β by
@gaborbernat,#1663π - Windows Store Python is supported now β and bestie, creation via symlink is not allowed, because that straight up does not work by design β by
@gaborbernat,#1709πͺ activate_this.pywas throwingAttributeErroron Windows when the virtual environment got created through the cross python mechanism β by@gaborbernat,#1710--no-pip,--no-setuptoolsand--no-wheelwere not being respected (rude!) β by@gaborbernat,#1712- Missing
.pyfiles are allowed when a compiled.pycversion is available β by@tucked,#1714 - Doesn't fail anymore if the distutils/setuptools patch lands on a C-extension loader (think
zipimporteron Python 3.7 or earlier) β by@gaborbernat,#1715 - Python 2 implementations that need the landmark files and
site.pyin the platform standard library instead of the virtual environment's standard library path are supported now β notably some RHEL ones, like the Docker imageamazonlinux:1β by@gaborbernat,#1719π§ - The test suite passes even when it's called with the system Python, which helps with repackaging the tool for Linux distributions β by
@gaborbernat,#1721 - Also generates the
pipx.yconsole script next topip-x.y, so it's compatible with how pip installs itself β by@gaborbernat,#1723 - The application data folder gets created automatically if it does not exist β by
@gaborbernat,#1728π
π Improved Documentation β 20.0.11
- The
supportssection (the#compatibility-requirementsanchor) now says explicitly which Python installations we support β by@gaborbernat,#1714β¨
π v20.0.10 (2020-03-10)
π Bugfixes β 20.0.10
- Acquiring python information could get altered by distutils configuration files, which generated virtual environments with an incorrect layout β fixed β by
@gaborbernat,#1663 - Embedded setuptools upgraded to
46.0.0from45.3.0on Python 3.5+ β by@gaborbernat,#1702
π Improved Documentation β 20.0.10
- Documented the requirements (pip + index server) when installing via pip, in the installation section β by
@gaborbernat,#1618 - Documented installing from non PEP-518 systems β
@gaborbernat,#1619 - Documented installing the latest unreleased version from Github β
@gaborbernat,#1620
πΈ v20.0.9 (2020-03-08)
π Bugfixes β 20.0.9
pythonw.exeworks aspython.exeon Windows β by@gaborbernat,#1686- Legacy loaders are handled for the virtualenv import hooks used to patch the distutils configuration load β by
@gaborbernat,#1690 - Support for python 2 platforms that store landmark files in
platstdliboverstdlib(e.g. RHEL) β by@gaborbernat,#1694 - Embedded setuptools upgraded to
45.3.0from45.2.0for Python 3.5+ β by@gaborbernat,#1699
π¦ v20.0.8 (2020-03-04)
π Bugfixes β 20.0.8
- Having distutils configuration files that set
prefixandinstall_scriptswas causing packages to install in the wrong location β those config files live athttps://docs.python.org/3/install/index.html#distutils-configuration-filesβ by@gaborbernat,#1663 PYTHONPATHwas being overridden on Python 2 β fixed β by@jd,#1673- Fixed list configuration value parsing from a config file or environment variable β by
@gaborbernat,#1674 - The Batch activation script's shell prompt now displays the environment name by default β by
@spetafree,#1679 - Startup on Python 2 was slower for virtualenv, and it was all because setuptools was calculating its working set distribution β fixed β by
@gaborbernat,#1682 - Entry points were not populated for editable installs on Python 2, since the setuptools working set got calculated before
easy_install.pthruns β fixed β by@gaborbernat,#1684 attr:import was failing for setuptools β fixed β by@gaborbernat,#1685
β v20.0.7 (2020-02-26)
π Bugfixes β 20.0.7
- Distutils fixup is disabled for python 3 until pypa/pip
#7778(https://github.com/pypa/pip/issues/7778) is fixed and released β by@gaborbernat,#1669
π v20.0.6 (2020-02-26)
π Bugfixes β 20.0.6
- The global site package was always being added with bundled macOs python framework builds β fixed β by
@gaborbernat,#1561 - Generated scripts were using host version info instead of target β fixed β by
@gaborbernat,#1600 - Circular prefix reference with single elements is fixed β these get accepted as if they were system executables, and it prints an info about them referencing themselves β by
@gaborbernat,#1632 - Handling for when the application data folder is read-only π:
- the application data folder is now controllable via
app-data(the#app-dataanchor), clear-app-datanow cleans the entire application data folder, not just the app-data seeder path,- it checks if the application data path passed in does not exist or is read-only, and falls back to a temporary directory,
- temporary directory application data is automatically cleaned up at the end of execution,
symlink-app-data(the#symlink-app-dataanchor) is alwaysFalsewhen the application data is temporary.
- the application data folder is now controllable via
β all of the above by @gaborbernat, #1640
- PyPy 2 builtin modules are imported from the standard library rather than from builtin β fixed β by
@gaborbernat,#1652 - Creation of entry points now works when the path contains spaces β by
@nsoranzo,#1660 - Relative paths fixed for the zipapp (for python 3.7+) β by
@gaborbernat,#1666
π v20.0.5 (2020-02-21)
β¨ Features β 20.0.5
- Also creates
pythonX.Xexecutables when creating pypy virtualenvs β by@asottile,#1612 - Fails with a better error message if you try to install source with unsupported setuptools, allows
setuptools-scm >= 2, and moves to the legacysetuptools-scmformat so older platforms (CentOS 7 and such) are supported better β by@gaborbernat,#1621 - The report of the created virtual environment is now split across four short lines instead of one long one β by
@gaborbernat,#1641
π Bugfixes β 20.0.5
- macOs Python 2 Framework support added (and yes, we now test it with the CI via brew) β by
@gaborbernat,#1561 - Fixed losing
libpypy-c.sowhen thepypyexecutable is a symlink β by@asottile,#1614 - The python interpreter is discovered in a case insensitive manner β by
@PrajwalM2212,#1624 - Cross interpreter support fixed for when the host python sets
sys.base_executablebased on__PYVENV_LAUNCHER__β by@cjolowicz,#1643
π v20.0.4 (2020-02-14)
β¨ Features β 20.0.4
- When aliasing interpreters, relative symlinks are used β by
@asottile,#1596
π Bugfixes β 20.0.4
- The use of
/as a pathname component separator is allowed on Windows β by vphilippon,#1582 - The minimal version of
sixrequired was lowered to1.9β by ssbarnea,#1606
π§ v20.0.3 (2020-02-12)
π Bugfixes β 20.0.3
- On Python 2 with Apple Framework builds, the global site package is no longer added when
system-site-packages(the#system-site-packagesanchor) is not specified β by@gaborbernat,#1561 - Fixed the system python discovery mechanism when prefixes contain relative parts (e.g.
..) β it now resolves paths within the python information query π ty @gaborbernat. (#1583) - Exposed a programmatic API as
from virtualenv import cli_runβ by @gaborbernat. (#1585) - Fixed the app-data seeder (
#seeder) injecting a extra.dist-info.virtualenvpath that breaksimportlib.metadataβ now we inject an extra.virtualenvinstead π by @gaborbernat. (#1589)
π Improved Documentation β 20.0.3 π
- Documented that programmatic API,
from virtualenv import cli_run, under Python (#programmatic-api) β by @gaborbernat. (#1585)
π v20.0.2 (2020-02-11) π
β¨ Features β 20.0.2 β¨
- It now prints out a one line message about the created virtual environment when verbose (
#verbose) is not set, and bestie you can silence that to get back the original behavior via the quiet (#quiet) flag β by @pradyunsg. (#1557) - virtualenv's app data cache can now be overridden by
VIRTUALENV_OVERRIDE_APP_DATAβ by @asottile. (#1559) - Passing in the virtual environment name/path is now required (it no longer defaults to
venv) β by @gaborbernat. (#1568) - Added a CLI flag
with-traceback(#with-traceback) that lets you see the stacktrace of the virtualenv when a failure occurs β by @gaborbernat. (#1572)
π Bugfixes β 20.0.2 π
- Long path names are supported for generated virtual environment console entry points (such as
pip) when using the app-data seeder (#seeder) β by @gaborbernat. (#997) - Improved the python discovery mechanism:
- it does not fail if there are executables that fail to query (e.g. because we don't have execute access to them) on the
PATH, - beside the prefix folder it also tries the platform dependent binary folder within that,
- it does not fail if there are executables that fail to query (e.g. because we don't have execute access to them) on the
by @gaborbernat. (#1545)
- When copying (either files or trees) it does not copy the permission bits, last access time, last modification time, and flags, because access to these might be forbidden (for example in the case of the macOs Framework Python) and they're not needed for the user to use the virtual environment β by @gaborbernat. (#1561)
- While discovering python executables, interpreters that cannot be queried are now displayed with info level rather than warning, so they're no longer shown by default (these can just be executables we don't have access to, or that are broken β don't warn if it's not the target Python we want) β by @gaborbernat. (#1574)
- The app-data seeder (
#seeder) no longer symlinks the packages on UNIX and copies on Windows. Instead it now always copies by default, but it now has thesymlink-app-data(#symlink-app-data) flag letting users request this less robust but faster method β by @gaborbernat. (#1575)
πΈ Improved Documentation β 20.0.2 πΈ
- Added a link to the legacy documentation (https://virtualenv.pypa.io/en/legacy) for the changelog by @jezdez. (#1547)
- Fine tuned the documentation layout: default width of theme, allowed tables to wrap around, soft corners for code snippets β by @pradyunsg. (#1548)
π¦ v20.0.1 (2020-02-10) π¦
β¨ Features β 20.0.1 β¨
- Upgraded embedded
setuptoolsto45.2.0from45.1.0for Python 3.4+ β by @gaborbernat. (#1554)
π Bugfixes β 20.0.1 π
- Virtual environments created via a relative path on Windows created bad console executables β by @gaborbernat. (#1552)
- Ok so sometimes venvs that were created set their base executable to themselves, and we accept these without question, which means we handle virtual environments as system pythons and that causes issues β by @gaborbernat. (#1553)
π v20.0.0. (2020-02-10) π
(yes the heading has a lil extra period at the end, we keep it, no notes π )
π Improved Documentation β 20.0.0. π
- Fixed typos, repeated words and inconsistent heading spacing. Rephrased parts of the development documentation and CLI documentation. Expanded shorthands like env var and config to their full forms. Used descriptions from the respective documentation for projects listed in related links β by @pradyunsg. (#1540)
β v20.0.0b2 (2020-02-04) β
β¨ Features β 20.0.0b2 β¨
- Improved the base executable discovery mechanism:
- it prints at debug level why we refuse some candidates,
- when no candidates match exactly, instead of hard failing it falls back 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 still working when the OS upgrade replaces/upgrades the system python with a never version than what the virtualenv host python was created with),
- it always resolves
system_executableinformation 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 the type of the virtualenv host python version instead of the system python's version β these two can differ if the OS upgraded the system python underneath and the virtualenv host was created via copy),
by @gaborbernat. (#1515)
- Generate
bashandfishactivators on Windows too (as these can be available with git bash, cygwin or mysys2) β by @gaborbernat. (#1527) - Upgraded the bundled
wheelpackage from0.34.0to0.34.2β by @gaborbernat. (#1531)
π Bugfixes β 20.0.0b2 π
- The Bash activation script should have no extensions instead of
.sh(this fixes the virtualenvwrapper (https://pypi.org/project/virtualenvwrapper) integration) β by @gaborbernat. (#1508) - Shows less information when we run with a single verbosity (
-v):- it no longer shows accepted interpreters information (as the last proposed one is always the accepted one),
- it does not display the
str_specattribute forPythonSpec, as these can be deduced from the other attributes, - for the app-data seeder it does not show the type of lock, only the path to the app data directory,
By @gaborbernat. (#1510)
- Fixed not being able to discover a python interpreter that has already been discovered under a different path (such is the case when we have multiple symlinks to the same interpreter) β by @gaborbernat. (#1512)
- Relative paths are now supported for
-pβ by @gaborbernat. (#1514) - Creating virtual environments in parallel failed with "cannot acquire lock within app data" β by @gaborbernat. (#1516)
pthfiles were not processed under Debian CPython2 interpreters β by @gaborbernat. (#1517)- Fixed the prompt not being displayed correctly with the upcoming
fish3.10, because we weren't preserving$pipestatusβ by @krobelus. (#1530) - Stable order within
pyenv.cfg, plusinclude-system-site-packagesis added only for creators that reference a global Python β by user:gaborbernat. (#1535)
πΈ Improved Documentation β 20.0.0b2 πΈ
- Created the first iteration of the new documentation β by @gaborbernat. (#1465)
- Project readme is now of type MarkDown instead of reStructuredText β by @gaborbernat. (#1531)
π« v20.0.0b1 (2020-01-28) π«
- First public release of the rewrite. Everything is brand new and just added. Iconic behavior honestly.
--downloaddefaults toFalse- No longer replaces the builtin site module with a custom version baked within virtualenv code itself (https://github.com/pypa/virtualenv/blob/legacy/virtualenv_embedded/site.py). A simple shim module is used to fix up things on Python 2 only.
β οΈ Warning: The current virtualenv is the second iteration of implementation. From version0.8all the way to16.7.9we numbered the first iteration. Version20.0.0b1is 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 π«Ά
Author name not set π (that's it, that's the whole field, bestie β literally not set)
π Copyright π
2007-2026, PyPA, PyPA
That's a wrap on the whole page, fr fr π Thanks for reading all the way to the very end β this was VIRTUALENV(1), version 21.14, dated October 12, 2026, and you absolutely ate that. Go forth and make some environments, ily, bye πβ¨
π¦ where this page comes from
- arch rolling: python-virtualenv 21.14.7-1 Β· MIT
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms