If you've been writing Python code for any amount of time, you've probably used or heard that you should use virtual environments, also commonly referred to as a venv on the command line. A virtual environment is a set of scripts that enable you to toggle the virtual environment on and off in your current shell. There's only two scripts that you need to know about:

  • activate -- activates the virtual environment by temporarily modifying your current shell's PATH, so that the virtual environment's paths are at the front and take precedence. This forces the use of "isolated" (i.e., higher precedence paths) versions of python, pip and installed packages.
  • deactivate -- deactivates the virtual environment by restoring your current shell's PATH back to normal

That's it. If you want to know why and how, read on.

What a venv Solves

By default, Python is installed system-wide and all Python code will run using the same, global Python environment. This is true when installing on Windows, and if you use macOS, Python is pre-installed system-wide in /usr/bin/python3. Additionally, Python's default package manager, pip, installs all its packages into the same system-wide environment as well.

All gravy until you're working on two different Python projects using your system-wide installation. The moment those two projects require different versions of Python or different versions of the same dependency, you're in a world of trouble because they share the same global environment. A virtual environment or venv provides a way to isolate your projects' Python environments and avoid conflicts between them.

How a venv Works

The official docs demonstrate how to create a virtual environment by using the built-in venv module:

python3 -m venv /path/to/new/virtual/environment

The convention is to create a virtual environment inside of a directory named venv within your existing project. Like so:


> cd my-project

> ls -l
.rw-r--r--@ 0 kai 15 minutes main.py

# Create inside `my-project`
> python3 -m venv venv

> ls -l
.rw-r--r--@ 0 kai 15 minutes main.py
drwxr-xr-x@ - kai 15 minutes venv

Some folks like to name theirs .venv, so that the directory is hidden. I personally don't like to hide the directory and would rather know it's there without second guessing.

Let's use tree to see what's inside the venv directory:

> tree -L3 venv
venv
β”œβ”€β”€ bin
β”‚Β Β  β”œβ”€β”€ activate
β”‚Β Β  β”œβ”€β”€ activate.csh
β”‚Β Β  β”œβ”€β”€ activate.fish
β”‚Β Β  β”œβ”€β”€ Activate.ps1
β”‚Β Β  β”œβ”€β”€ pip
β”‚Β Β  β”œβ”€β”€ pip3
β”‚Β Β  β”œβ”€β”€ pip3.14
β”‚Β Β  β”œβ”€β”€ python -> python3.14
β”‚Β Β  β”œβ”€β”€ python3 -> python3.14
β”‚Β Β  β”œβ”€β”€ python3.14 -> /opt/homebrew/opt/python@3.14/bin/python3.14
β”‚Β Β  └── πœ‹thon -> python3.14
β”œβ”€β”€ include
β”œβ”€β”€ lib
β”‚Β Β  └── python3.14
β”‚Β Β      └── site-packages
└── pyvenv.cfg

6 directories, 12 files

There's the activate script. Its full path is venv/bin/activate. It's not executable so you must source it:

> source venv/bin/activate

The deactivate command isn't present in the listing above because it's automatically defined by the activation script and made available in your current shell.

That's it, folks. Python's virtual environments are just your shell's search path doing exactly what it was designed to do.

End of the PATH

This wraps up the "Follow the PATH" series. We started with the basics of how PATH works, learned how to jump around directories efficiently with CDPATH and Zoxide, and now you understand how Python virtual environments leverage the same fundamental concept to isolate project dependencies.

Lastly, now that you know how virtual environments work and how to create them, most folks have migrated to using uv to manage their virtual environments. You'll be more productive if you're already familiar with something you're guaranteed to come across.

Relevant Music

Now that you know the PATH in and out, you are ready for your voyage.