How Python’s Virtualenv Works

Mike Taylor
4 min readJan 12, 2020

--

What is a virtual environment?

In both Windows and Linux environments, there’s nothing virtual about a python virtual environment. Since python is compiled, and then you run scripts that use the compiled python interpreter, it’s not conducive to compile it every time you need a new virtual environment. When creating a new virtual environment, you’re not getting a fresh python interpreter; you’re using the environment variables to point your compiled interpreter at a specific interpreter, libraries, and scripts.

With python, there are three primary environment variables for selecting the interpreter, libraries, and scripts that will be available: PYTHONHOME, PYTHONPATH, and PATH. Each of these variables affects how your python interpreter looks for the libraries and scripts that will be available.

PYTHONHOME — This environment variable will determine the folder that your shell looks at for the python interpreter. It will affect which site-packages folder is used if there’s a site-packages folder in the subdirectories.

PYTHONPATH — This environment variable tells the current python interpreter where it can look for additional libraries that you want to be able to import that may not be in the standard site-packages, or if you’re going to add a separate site-packages directory.

PATH — This environment variable tells you where to look for scripts (Or python’s interpreted equivalent of executables), like the scripts you’d end up building and installing with python packages.

A virtual environment is simply a set of folders that point to site-packages and a python interpreter and an essential “activation” script that updates the environment variables PYTHONHOME and PATH to look at these folders. There’s a bit more magic and a bunch of options when setting that up, but that’s the gist of it.

TL; DR

The virtual environment is literally just setting PATH to look in your virtual environment folders before it looks elsewhere and backing up the old PATH so it can be restored. It also backs up other environment variables related to python and will restore them when the deactivate script is run. There is nothing virtual about it — it just tells python to look somewhere else first.

Walking through the activate script

Now that we know there’s nothing virtual about a virtual environment, I’ll give a little bit more context to that statement by walking through the activation script in Linux. Still, the principle is the same in windows — it just changes the syntax for the command line script that changes the environment variables. There are two things you do with the activation script — you run it, and you deactivate it.

The deactivate function

When you run the activate script, the very first thing that happens is a function is defined for your shell session. The function that’s created is called “deactivate”. This script’s purpose is to look for specific environment variables, and if they exist, swap them with the current ones. The script for bash is below:

deactivate () {# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
hash -r
fi

if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENVif [ ! "$1" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}

This script is looking for _OLD_<something>, and if it’s there, set it to a current <something> and unset the _OLD_<something> version. After it’s run and returns variable to their old state, it removes itself from the shell (You’ll no longer have the deactivate function defined).

After the deactivate function is defined

Immediately after the deactivate function is defined, it’s run in “nondestructive” mode, meaning it doesn’t delete itself after being run. Running this function first makes sure that if you had one of these variables set from an old virtual environment, it will unset them and get you back to your “normal” environment variables configuration before making modifications for a new virtualenv.

# unset irrelevant variables
deactivate nondestructive

Virtual environments are not designed to be moved around because they are specific to a compiled interpreter binary that supports particular scripts. When installing scripts into a virtual environment, Python2 and Python3 will have very different supported methods — so unless you explicitly manipulate it, the virtual environment will be static in its current location. That static location is defined in the activate script with a full static path to the virtualenv:

VIRTUAL_ENV="/mnt/c/Users/bubth/Development/medium_venv/venv"
export VIRTUAL_ENV

Now we’re at the first magic part of our activate script, updating our PATH variable.

_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH

What it’s doing first is saving your current $PATH, and then putting the virtualenv path first. This is called a shim. From now on, your shell will look for executables in the virtualenv first.

After the PATH variable is updated, the script will check for PYTHONHOME. If PYTHONHOME is set, it will save the PYTHONHOME and unset it so that it can be restored later, but will not interfere with the current virtual environment if it was defined.

The rest of the script is a matter of convenience that gives you a virtual environment name in your command prompt and some housekeeping.

--

--

No responses yet