Learn why virtual environments are essential for Python development and how to create, activate, and manage them effectively.
Every Python developer eventually runs into the same painful problem: one project needs version 2 of a library, another needs version 4, and installing them globally means one breaks the other. Virtual environments solve this completely by giving each project its own isolated set of packages. Understanding them is foundational — not advanced, just essential — and once the concept clicks, a whole category of "it works on my machine" problems disappears for good.
By default, Python installs packages globally, shared across every project on your machine. This seems convenient until two projects need different versions of the same library, at which point installing one version breaks the other, because there is only one global slot for it. As you work on more projects, this conflict becomes inevitable, and the global package set turns into a tangled mess where upgrading a dependency for one project silently breaks another. Virtual environments exist precisely to eliminate this: each project gets its own isolated environment, so its dependencies never collide with any other project's.
A virtual environment is, at its core, a self-contained directory holding its own Python interpreter reference and its own site-packages folder where libraries are installed. When you activate it, your shell uses that environment's packages instead of the global ones, so pip install puts libraries there and your code imports from there. It is not a virtual machine or a container — it is much lighter, just a directory and some path manipulation. Grasping that it is simply an isolated package directory your tools point at demystifies the whole concept and makes the commands that follow intuitive rather than magical.
Python includes virtual environment support built in through the venv module, so you need nothing extra to start:
python -m venv .venv
This creates a .venv directory in your project containing the isolated environment. The name .venv is a common convention, and keeping the environment inside the project folder makes it easy to find and manage. That single command is all it takes to create a clean, isolated space for your project's dependencies, separate from your global Python and from every other project.
A virtual environment is always built from an existing Python installation, and it keeps using that interpreter's version for its whole life. So the very first step is to check which Python you actually have, and what command starts it on your system. This is where the three major operating systems differ most, and where a lot of first-day confusion comes from.
On Windows, the installer from python.org includes the py launcher, which knows about every Python version you have installed and lets you pick one explicitly. On macOS and Linux, the command is usually python3; plain python may not exist at all, or may point at something unexpected. Run these checks in a terminal before creating anything:
# Windows (PowerShell or Command Prompt)
py --list
py -3.12 --version
# macOS and Linux
python3 --version
which python3
If the version shown is older than the one your project needs, install a newer Python first. Creating an environment does not download or upgrade Python — it only copies or links the interpreter you already have. An environment made with Python 3.11 stays a 3.11 environment forever; to move a project to 3.12 you create a fresh environment with 3.12 and reinstall the packages from requirements.txt.
Open PowerShell, move into your project folder, and create the environment using the launcher so there is no doubt about which version you get:
cd C:\Users\deploy\projects\myproject
py -3.12 -m venv .venv
dir .venv
Listing the folder shows three things: a Scripts folder (containing python.exe, pip.exe and the activation scripts), a Lib\site-packages folder (where your libraries will be installed), and a small text file called pyvenv.cfg. If you see those, the environment was created correctly.
Open Terminal (the default shell on current macOS is zsh), move into the project and create the environment with python3:
cd ~/projects/myproject
python3 -m venv .venv
ls .venv
On macOS and Linux the layout is slightly different: executables live in .venv/bin instead of Scripts, and packages go into .venv/lib/python3.12/site-packages, with the version number in the path. If you installed Python through Homebrew, make sure the python3 you run is the Homebrew one (which python3 usually shows a path under /opt/homebrew/bin on Apple Silicon Macs) rather than an older system copy.
Most distributions ship Python already, but Debian and Ubuntu split the venv support into a separate package. The commands are the same as on macOS:
cd ~/projects/myproject
python3 -m venv .venv
ls .venv
If this prints an error saying that ensurepip is not available and suggests installing a python3-venv package, that is expected on a fresh Debian or Ubuntu system. Install it once with sudo apt install python3-venv (or the version-specific name the message shows, such as python3.12-venv), delete the half-created .venv folder, and run the command again. Fedora and Arch include venv in their main Python package, so the extra step is not needed there.
The pyvenv.cfg file is the environment's identity card, and reading it is a quick way to learn which Python an environment was built from. It is plain text and contains lines like these:
home = /usr/bin
include-system-site-packages = false
version = 3.12.3
home points at the directory of the base interpreter, and include-system-site-packages = false is the setting that keeps globally installed packages invisible inside the environment. When Python starts, it looks for this file next to (or one level above) its own executable; finding it is what tells the interpreter "I am running inside a virtual environment, use this folder's site-packages". That is the whole trick — there is no background service and nothing registered with the operating system.
A few flags of python -m venv are worth knowing from the start:
--upgrade-deps upgrades pip inside the new environment immediately, so you do not start with an outdated copy that prints upgrade warnings.--prompt myproject sets the label shown in your shell prompt when the environment is active; without it, the folder name (.venv) is used, which is less helpful when every project has a .venv.--clear deletes the contents of an existing environment directory before creating it again — a fast way to start over.python3 -m venv --upgrade-deps --prompt myproject .venv
Creating the environment is not enough — you must activate it so your shell uses it. Activation differs slightly by operating system and shell, but the effect is the same: your prompt changes to show the active environment, and python and pip now refer to the isolated versions rather than the global ones. When you are done working on the project, deactivate returns you to the global environment. Activation is the step beginners most often forget, leading to packages being installed globally by mistake, so making it a habit to activate before working on a project is key.
Activation is done by running a small script that the environment created for you. Which script you run depends on your operating system and your shell, and using the wrong one is a frequent source of "nothing happened" or "command not found" errors:
| System and shell | Activate |
|---|---|
| Windows PowerShell | .venv\Scripts\Activate.ps1 |
| Windows Command Prompt | .venv\Scripts\activate.bat |
| Git Bash on Windows | source .venv/Scripts/activate |
| macOS / Linux, bash or zsh | source .venv/bin/activate |
| fish shell | source .venv/bin/activate.fish |
| csh / tcsh | source .venv/bin/activate.csh |
Note the word source on macOS and Linux. Running .venv/bin/activate directly, without source, starts a separate child shell that applies the changes and immediately exits, so your own shell is left untouched. source (or its shorter synonym .) runs the script inside your current shell, which is what makes the changes stick.
Do not rely only on the prompt prefix — confirm it. Ask your shell which python it will run, and ask Python where it thinks it lives:
# macOS / Linux
source .venv/bin/activate
which python
python -c "import sys; print(sys.prefix)"
# Windows PowerShell
.venv\Scripts\Activate.ps1
Get-Command python
python -c "import sys; print(sys.prefix)"
Both answers should point inside your project's .venv folder. Notice that once an environment is active, plain python works on every platform, even on macOS and Linux where you needed python3 before: the environment always provides a python command. If the path still shows a system location such as /usr/bin/python3 or C:\Program Files\Python312, activation did not happen in this terminal window. Remember that activation is per terminal: every new tab or window starts deactivated, and you need to activate again there.
The activation script does only a handful of things, all of them temporary and limited to the current shell session. It puts the environment's bin (or Scripts) folder at the front of the PATH variable, sets a VIRTUAL_ENV variable holding the environment's path, changes your prompt, and defines a deactivate command that undoes all of it. Tools such as editors and shell themes read VIRTUAL_ENV to show which environment is active. Closing the terminal discards all of these changes, which is why you never need to "clean up" after an environment you forgot to deactivate.
With the environment activated, pip install places packages into that environment's isolated site-packages rather than globally:
pip install django requests
Because the environment is isolated, you can install any versions this project needs without worrying about other projects, and you can experiment freely knowing that deleting the environment removes everything cleanly. This is the payoff of the whole exercise: package installation becomes a per-project concern, where each project's dependencies are exactly what it needs, nothing more, with no risk of affecting anything outside it.
There is a small habit that prevents a whole class of mistakes: install with python -m pip instead of plain pip. The pip command is just whatever program called pip appears first on your PATH, which may belong to a different Python than the one you run your code with. python -m pip runs pip as a module of the exact interpreter named python, so packages always land where that interpreter will import them from.
python -m pip install "requests>=2.32,<3"
python -m pip list
python -m pip show requests
The quotes around the version specifier matter: without them, some shells interpret > and < as redirection and silently create files with odd names instead of installing the version you asked for. pip list shows everything installed in the active environment, and pip show prints details about one package, including its Location — a quick way to prove it really is inside .venv.
Package management inside an environment works the same way as globally, just safely contained. python -m pip install --upgrade requests moves to the newest release allowed, and python -m pip uninstall requests removes it. Be aware that uninstalling a package does not remove the dependencies it pulled in; they stay behind as orphans. For a beginner the easiest cure is the one described later in this tutorial: delete the environment and recreate it from the requirements file, which gives you exactly the listed packages and nothing else.
To make a project reproducible, you record its dependencies in a file, conventionally requirements.txt, listing each package and ideally its version. You generate it from the current environment and others recreate the same setup from it:
pip freeze > requirements.txt
pip install -r requirements.txt
This is how a project's dependencies travel — a teammate or a server creates a fresh virtual environment and installs from the requirements file to get exactly the same packages. The combination of an isolated environment and a recorded dependency list is what makes a Python project reliably reproducible across machines.
The file produced by pip freeze is plain text with one package per line and an exact version after ==. It lists not only what you installed yourself, but also every dependency those packages pulled in. After installing just Django and requests, for example, the file will look roughly like this (your version numbers will differ):
asgiref==3.8.1
certifi==2024.8.30
charset-normalizer==3.4.0
Django==5.2
idna==3.10
requests==2.32.3
sqlparse==0.5.1
urllib3==2.2.3
Beginners are often surprised to see eight packages when they installed two. That is normal: Django needs asgiref and sqlparse, and requests needs certifi, charset-normalizer, idna and urllib3. Recording all of them with exact versions is what makes the environment reproducible — a new machine gets precisely the same set, not just the same top-level names.
Because pip freeze prints whatever is installed where pip is running, always run it with the project's environment active. Run it by accident against your global Python and you will capture dozens of unrelated packages from other experiments, which then get installed on every teammate's machine. A quick glance at the file before committing it catches this: if you see packages your project has never used, regenerate it from the correct environment.
Some teams also keep a short, hand-written list of only the direct dependencies (for example a file with just Django>=5.2,<6.0 and requests>=2.32,<3) alongside the frozen file. The short list says what the project intends to depend on; the frozen list records exactly what was tested. You do not need this split on day one, but it explains why you will see both styles in real projects.
A common beginner mistake is committing the virtual environment directory itself to version control. You should not — the environment can be large, is specific to your operating system, and is fully reproducible from the requirements file. Instead, add the environment directory to your .gitignore and commit only the requirements file. Anyone who clones the project recreates the environment locally from the requirements. This keeps your repository small and portable, and reinforces the right mental model: the environment is a local, disposable artifact, while the requirements file is the portable record of what the project needs.
Create a file named .gitignore at the root of the project (next to requirements.txt) with at least these lines:
# virtual environments
.venv/
venv/
env/
# Python bytecode caches
__pycache__/
*.pyc
If the environment was already committed before you added this rule, ignoring it is not enough, because Git keeps tracking files it already knows about. Remove it from the repository (but not from your disk) with git rm -r --cached .venv, then commit. As a convenience, Python 3.13 and later also write a tiny .gitignore inside every new environment folder, so Git skips it automatically, but adding your own rule keeps older Python versions and other tools covered.
The everyday rhythm is simple once it is a habit: open a project, activate its environment, work, and deactivate when switching to something else. Each project has its own environment, so switching projects means switching environments, and you never install into the global Python. New project means create a new environment; new dependency means install it with the environment active and update the requirements file. This small discipline — one environment per project, always activated before working — prevents the dependency conflicts that plague developers who skip it, and it quickly becomes second nature.
Reading about environments is useful, but doing the whole cycle once end to end is what makes it stick. This example builds a tiny script that downloads a web page and prints its status code, records its dependency, deletes the environment, and rebuilds it — the full loop you will repeat in every real project. Follow the commands for your own operating system; everything after activation is identical everywhere.
# macOS / Linux
mkdir -p ~/projects/myproject && cd ~/projects/myproject
python3 -m venv --upgrade-deps .venv
source .venv/bin/activate
# Windows PowerShell
mkdir C:\Users\deploy\projects\myproject
cd C:\Users\deploy\projects\myproject
py -3.12 -m venv --upgrade-deps .venv
.venv\Scripts\Activate.ps1
After activation your prompt starts with (.venv). From here on, use python on every platform.
python -m pip install requests
Create a file named check_site.py in the project folder with your editor:
import sys
import requests
def main() -> None:
url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com"
response = requests.get(url, timeout=10)
print(f"{url} answered with status {response.status_code}")
print(f"running on: {sys.executable}")
if __name__ == "__main__":
main()
Run it with python check_site.py. The second line of output is the interesting one for learning purposes: sys.executable prints the full path of the interpreter running the script, and it should end in .venv/bin/python or .venv\Scripts\python.exe. That is direct proof that your code is using the isolated environment.
Now deactivate and run the same script again with the global Python (python3 check_site.py on macOS/Linux or py check_site.py on Windows). Unless you happen to have requests installed globally, it fails with ModuleNotFoundError: No module named 'requests'. This failure is the whole point: the package exists only inside the project's environment, and nothing leaked out into the rest of your system.
# with the environment active again
python -m pip freeze > requirements.txt
deactivate
# macOS / Linux: delete and rebuild
rm -rf .venv
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
# Windows PowerShell: delete and rebuild
Remove-Item -Recurse -Force .venv
py -3.12 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
Run python check_site.py one last time. It works again, with the identical versions recorded in requirements.txt. You have just done, on your own machine, exactly what a teammate or a deployment server does when it sets up the project: a clean environment plus the requirements file equals a working project.
Activation is a convenience for interactive terminal work, not a requirement. You can always run the environment's interpreter directly by its path, and it behaves exactly as if the environment were active — it uses its own site-packages because it finds its own pyvenv.cfg. This is how environments are typically used in scripts, scheduled jobs and servers, where there is no interactive shell to activate in:
# macOS / Linux
/srv/app/.venv/bin/python manage.py migrate
/srv/app/.venv/bin/python -m pip install -r /srv/app/requirements.txt
# Windows
C:\Users\deploy\projects\myproject\.venv\Scripts\python.exe check_site.py
This also explains why activation is "just" a PATH change: all it saves you is typing the full path. For a cron job, a systemd service or a Windows scheduled task, always point at the environment's interpreter by absolute path instead of trying to run an activation script first. Console commands installed by packages (for example django-admin) live in the same bin or Scripts folder and can be called by full path the same way.
While venv and pip are the built-in foundation and entirely sufficient, the ecosystem has tools that build on the same idea with extra conveniences — managing environments and dependencies together, handling lock files for exact reproducibility, or speeding up installation. These tools are worth exploring as you grow, but they do not change the underlying concept, which remains per-project isolation. Starting with the built-in tools gives you a solid understanding of what is actually happening, so that if you later adopt a higher-level tool, you understand the isolation it is managing for you rather than treating it as a black box.
Some Python packages are applications you want to run from anywhere rather than libraries a project imports — code formatters, linters, or project generators. Installing those into each project environment is wasteful, and installing them globally brings back the conflicts. pipx solves this by giving each tool its own hidden virtual environment and exposing only its command on your PATH. For example, pipx install ruff makes the ruff command available everywhere while keeping its dependencies isolated. It is the virtual environment idea applied to tools instead of projects.
A popular newer tool is uv, which performs the same jobs as venv and pip but much faster, and can also install Python versions for you. Its basic commands mirror what you have learned here — uv venv creates a .venv folder, and uv pip install -r requirements.txt installs into it — and the result is an ordinary virtual environment that the standard activation scripts and editors understand. That is exactly why learning the built-in tools first pays off: the folder, the pyvenv.cfg, the site-packages and the activation step are all the same, only the speed and conveniences differ.
| Tool | Good for | Comes with Python? |
|---|---|---|
venv + pip | Learning, any project, any machine | Yes |
pipx | Installing command-line tools in isolation | No |
uv | Fast installs, managing Python versions, lock files | No |
A few issues recur for newcomers. Forgetting to activate the environment, so packages install globally and the isolation is lost. Committing the environment directory instead of ignoring it. Not recording dependencies, so the project cannot be reproduced elsewhere. Mixing global and environment installs and getting confused about where a package lives. Each of these traces back to not yet having internalized the one-environment-per-project, always-activated model, and each disappears once that habit is established. Recognizing these patterns helps you diagnose the confusing "but I installed it" moments that almost everyone hits while learning.
The general pitfalls above show up in practice as specific error messages. Here are the ones almost every beginner meets, what each one really means, and the fix. Keep this section handy — when something goes wrong, the exact wording of the error is usually enough to find the right entry.
This is the classic "but I installed it" moment. The package is installed — just into a different Python than the one running your code. Typical causes: you installed while the environment was not active, you opened a new terminal and forgot to activate there, or your editor is using a different interpreter. Diagnose it by comparing the two sides:
python -c "import sys; print(sys.executable)"
python -m pip --version
Both lines must show paths inside the same .venv. The second command prints the pip version followed by the location it installs to. If they disagree, activate the environment and reinstall with python -m pip install, which guarantees the package goes to the interpreter you just checked.
PowerShell refuses to run Activate.ps1 because the default execution policy on many Windows machines blocks all local scripts. This is a safety setting, not a problem with your environment. The usual fix is to allow locally created scripts for your user account only, which does not need administrator rights:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
RemoteSigned still requires scripts downloaded from the internet to be signed, so it is a reasonable balance. If you are not allowed to change the policy (on a managed work laptop, for example), use Command Prompt with activate.bat instead, or skip activation and call .venv\Scripts\python.exe directly.
Windows ships small placeholder commands called app execution aliases: typing python before a real Python is on the PATH opens the Store page instead. Either use the py launcher, which the python.org installer always provides, or turn off the python.exe and python3.exe entries under Settings → Apps → Advanced app settings → App execution aliases. After activation this problem disappears for that terminal anyway, because the environment's own python.exe comes first on the PATH.
Recent Debian, Ubuntu, Arch Linux and Homebrew Python installations mark the system Python as managed by the operating system's package manager, and pip refuses to install into it. The message is actually good advice in disguise: it is telling you to use a virtual environment. Create and activate one, and the error goes away because you are no longer touching the system Python. Do not reach for the --break-system-packages flag the message mentions — it exists for special cases and, as the name says, can damage tools your operating system depends on. For command-line applications you want available everywhere, use pipx, covered in the tools section.
On Debian and Ubuntu, venv support is packaged separately, as described in the Linux walkthrough. Install python3-venv (or the version-specific package named in the message), remove the incomplete .venv directory, and create the environment again. Reusing the broken half-created folder usually leads to an environment without pip.
Virtual environments are not relocatable. The activation scripts store the environment's absolute path, and on macOS and Linux the scripts in bin (such as pip) begin with a line pointing at the old interpreter path. After moving or renaming the project folder you may see "bad interpreter: No such file or directory" or a pip that installs into the wrong place. Do not try to edit the paths by hand: delete .venv and recreate it from requirements.txt. The same applies after upgrading or uninstalling the base Python the environment was built from.
If pip install fails with a permission error, you are almost certainly installing into the system Python, which belongs to the administrator. Using sudo pip install "fixes" the error by writing files into system directories, where they can conflict with the operating system's own packages and are painful to remove. The correct fix is always to activate your project's environment first; it lives in your own folder, so no elevated permissions are ever needed.
| Symptom | Likely cause | Fix |
|---|---|---|
No (.venv) in the prompt | Not activated in this terminal | Run the activation command for your shell |
| Import works in terminal, fails in editor | Editor uses another interpreter | Select .venv as the project interpreter |
pip and python show different paths | Stale pip on PATH | Use python -m pip |
| Environment worked yesterday, broken today | Folder moved or base Python upgraded | Delete and recreate from requirements |
Hundreds of lines in requirements.txt | Frozen from global Python | Activate the project environment and freeze again |
It helps to understand the mechanism behind the magic. When you activate an environment, your shell's path is adjusted so that the environment's python and pip come first, ahead of the global ones. Packages install into the environment's own folder, and imports resolve from there. Nothing about the global Python is changed — the environment simply intercepts which interpreter and packages are used while it is active. This is why activation matters so much and why deactivating cleanly restores the global setup: the isolation is a matter of which directory your tools look in, switched on and off by activation rather than by altering anything permanent.
The cardinal rule is one environment per project, not one shared environment for everything. A shared environment recreates exactly the global-conflict problem virtual environments exist to solve, because two projects would again share one package set. Giving each project its own environment means each has precisely the dependencies it needs at the versions it needs, fully isolated from every other project. This discipline is what delivers the benefit, and it is worth being strict about: when you start a new project, the first step is creating its own environment, so its dependencies never entangle with anything else you are working on.
The combination of an isolated environment and a recorded requirements file is what makes a project reproducible anywhere — your laptop, a teammate's machine, a production server. Each creates a fresh environment and installs from the same requirements file to get an identical set of packages. This solves the classic "works on my machine" problem at the dependency level, because everyone is running the same versions rather than whatever happened to be installed globally. Reproducibility is not a nice-to-have; it is what lets a project move reliably between environments, and virtual environments plus a requirements file are the foundation that provides it.
A liberating property of virtual environments is that they are disposable. Because everything a project needs is recorded in its requirements file, the environment directory itself is throwaway — if it gets into a confusing state, you can simply delete it and recreate it fresh from the requirements. There is no need to carefully repair a broken environment when recreating it is so cheap. This disposability encourages healthy experimentation: you can try installing things, and if it goes wrong, reset to a clean state in seconds. Understanding that the environment is a regenerable artifact, not something precious to protect, removes a lot of the anxiety beginners feel about breaking their setup.
Your code editor needs to know about your virtual environment to give you accurate autocomplete, error checking, and the ability to run your code with the right packages. Most editors detect a project's environment automatically or let you select it, after which they use that environment's interpreter and see its installed packages. Pointing your editor at the project's environment is what makes the development experience smooth — the editor understands exactly the libraries your project uses. This integration is part of why keeping the environment inside the project folder is convenient, since editors look there, and it ties the isolation you have set up into the tools you work in every day.
In VS Code with the Python extension installed, open the Command Palette (Ctrl+Shift+P on Windows and Linux, Cmd+Shift+P on macOS), run Python: Select Interpreter, and choose the entry pointing at your project's .venv. If it is not listed, pick "Enter interpreter path" and browse to .venv/bin/python or .venv\Scripts\python.exe. New integrated terminals then activate the environment automatically. In PyCharm, the interpreter is chosen in the project's settings under the Python interpreter page, where you can add an existing environment by pointing at the same file.
When the editor still underlines imports in red after you installed a package, it is nearly always looking at the wrong interpreter. Check the interpreter shown in the status bar, and if it is correct, reload the window so the editor re-scans site-packages. Jupyter notebooks follow the same rule: the notebook's kernel must be the environment's Python, otherwise packages installed in .venv are invisible to it.
Virtual environments solve a problem every Python developer faces — conflicting dependencies between projects — by giving each project its own isolated set of packages. A virtual environment is simply a directory with its own package folder that your tools point at when activated; you create one with python -m venv, activate it before working, install packages into it in isolation, and record those packages in a requirements.txt so the project is reproducible anywhere. Never commit the environment itself — ignore it and commit the requirements file instead. Adopt the daily habit of one environment per project, always activated, and the dependency conflicts that frustrate so many beginners simply never occur. It is a foundational skill that, once second nature, quietly underpins clean, reproducible Python development for the rest of your career.