A Python virtual environment doesn’t actually copy your Python installation; it cleverly uses symlinks to point to your system Python, making it appear as a self-contained environment.

Let’s see this in action. First, create a virtual environment:

python3 -m venv myenv

Now, activate it:

source myenv/bin/activate

Your prompt changes, indicating you’re inside myenv. Let’s inspect the contents:

ls -l myenv

You’ll see directories like bin (or Scripts on Windows), include, lib (or Lib), and pyvenv.cfg.

Now, look at the bin directory:

ls -l myenv/bin

You’ll find python, pip, and other executables. Let’s check what myenv/bin/python actually is:

ls -l myenv/bin/python

On Linux/macOS, you’ll see something like:

lrwxrwxrwx 1 user user 15 Jan 1 10:00 myenv/bin/python -> /usr/bin/python3.10

This is a symbolic link (symlink) pointing to your system’s Python interpreter. myenv/bin/pip will also be a symlink, usually pointing to a script within myenv/bin/ that invokes the correct pip for your environment.

The pyvenv.cfg file is crucial:

[base-executable]
executable = /usr/bin/python3.10
python_version = 3.10.6
[install-scripts]
scripts-dir = bin

This configuration file tells the virtual environment where its "base" Python interpreter lives on the system. When you run python within the activated environment, it’s actually executing /usr/bin/python3.10 (or whatever your system Python is). The activation script (activate) modifies your PATH environment variable to prepend myenv/bin, ensuring that when you type python or pip, the system finds the versions within myenv/bin first. These are the symlinks that point to your system Python and the installed packages within the virtual environment’s lib directory.

The include directory contains header files needed for compiling Python extensions, and the lib directory is where installed packages will reside. When you pip install somepackage, the package files are placed in myenv/lib/pythonX.Y/site-packages/. Because the python executable is linked to your system Python, and that Python executable knows to look in myenv/lib/pythonX.Y/site-packages/ for installed modules (due to how Python’s import system is configured by venv and the activation script), it can find and import these packages.

The magic happens during activation. The activate script (found in myenv/bin/) does a few key things:

  1. Modifies PATH: It prepends myenv/bin to your PATH environment variable. This means when you type a command like python, the shell searches myenv/bin first, finds the symlink, and executes your system Python.
  2. Sets VIRTUAL_ENV: It sets the VIRTUAL_ENV environment variable to the absolute path of your virtual environment directory (/path/to/myenv). This variable is used by tools like pip and setuptools to determine the root of the virtual environment.
  3. Overrides PS1: It modifies your shell prompt (PS1) to visually indicate that you are inside a virtual environment (e.g., (myenv) user@host:~$).

When you uninstall a package, pip simply removes the corresponding files from myenv/lib/pythonX.Y/site-packages/. It doesn’t touch your system Python installation at all. This isolation is the core benefit.

What most people miss is how the pyvenv.cfg file, combined with the symlinks and the PATH modification, creates the illusion of a separate Python installation. The venv module essentially creates a lightweight wrapper around your existing Python interpreter, redirecting where it looks for executables and installed packages. It’s not a full copy, but a sophisticated pointer system.

The next step is understanding how to manage dependencies effectively across different projects using requirements files.

Want structured learning?

Take the full Python course →