Creating a virtual environment is the most surprisingly effective way to isolate Python projects, preventing dependency conflicts that can otherwise derail your development.

Let’s see it in action. Imagine you have two projects, project_a and project_b, each requiring different versions of a library, say requests.

First, create a virtual environment for project_a:

cd /path/to/project_a
python3 -m venv venv

This command creates a venv directory within your project_a folder. Inside, you’ll find a copy of the Python interpreter and pip, isolated from your system’s global Python installation.

Now, activate it:

source venv/bin/activate

Your shell prompt will change to (venv) /path/to/project_a$, indicating the environment is active. Any pip install commands will now install packages only into this venv directory.

(venv) /path/to/project_a$ pip install requests==2.25.0
(venv) /path/to/project_a$ pip freeze
requests==2.25.0
Flask==1.1.2

Now, switch to project_b and create its own environment, installing a different requests version:

cd /path/to/project_b
python3 -m venv venv
source venv/bin/activate
(venv) /path/to/project_b$ pip install requests==2.28.1
(venv) /path/to/project_b$ pip freeze
requests==2.28.1
Django==3.2.10

Notice how project_b has requests==2.28.1, while project_a still has requests==2.25.0. They coexist peacefully because their dependencies are managed within their respective venv directories.

The core problem virtual environments solve is dependency hell. Without them, installing requests==2.28.1 for project_b would overwrite the requests==2.25.0 needed by project_a, potentially breaking project_a. Virtual environments provide a sandboxed space for each project’s dependencies, ensuring that changes in one project don’t affect others.

Internally, python3 -m venv venv creates a directory structure containing:

  • bin/ (or Scripts/ on Windows): Contains the activated shell scripts, pip, and the Python executable specific to this environment.
  • lib/pythonX.Y/site-packages/: This is where installed packages reside. When the environment is active, pip and Python are configured to look only in this site-packages directory for modules, bypassing the global Python installation.

The activate script works by modifying your shell’s environment variables. Specifically, it prepends the venv/bin directory to your PATH and sets VIRTUAL_ENV to the path of the virtual environment. This ensures that when you type python or pip, you’re executing the versions from within your activated venv.

When you deactivate (simply by typing the command), these environment variables are restored to their previous state, returning you to your system’s global Python environment.

The pip freeze > requirements.txt command is crucial for reproducibility. It captures the exact versions of all installed packages into a file. Later, another developer (or you on a different machine) can recreate the exact same environment by running pip install -r requirements.txt within a newly created and activated virtual environment.

One common misconception is that virtual environments are only for complex projects with many dependencies. Even a simple script can benefit, preventing unexpected behavior if a system-wide package is updated. The overhead of creating and activating an environment is negligible compared to the time saved debugging dependency conflicts. It’s good practice to always use a virtual environment for every Python project, no matter how small.

Understanding how pip resolves package versions within an activated environment is key to debugging installation issues.

Want structured learning?

Take the full Pip course →