Python: Virtual Environments & Dependency Management

A Python virtual environment, or venv for short, gives every project its own private box of packages, so two projects can need two different versions of the same library and neither one breaks. This post shows you why virtual environments exist, how activation quietly changes your PATH, how to pin dependencies in requirements.txt, and how to stop one project from ever poisoning another again.

“The Python development world is filled with smart engineers, but dependency management can humble even the best of them.”

Kenneth Reitz, Hitchhiker’s Guide to Python

Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 13 minutes

Picture two developers sharing one laptop: Niranjan is building a scraper that needs requests version 2.28, and Viraj is building a web app that needs requests version 2.31. If they install both into the same Python, one of them loses. Python keeps only one version of a package installed at a time, so the second install quietly overwrites the first, and now one project is running on a library it was never tested against. A virtual environment fixes this by handing each project its own private copy of Python and its own site-packages folder.

Here is the everyday version. Think of a shared kitchen where everyone dumps their ingredients into one pantry. The moment two recipes need different brands of the same spice, someone’s dish gets ruined. A virtual environment is like giving each cook their own labelled box of ingredients. Same kitchen, same stove, but the supplies never get mixed up. That box is your project’s site-packages folder, and nothing you install for one project ever leaks into another.

Under the hood a Python virtual environment is not magic and it is not a container. It is just a directory that holds a link back to your real Python plus a fresh, empty site-packages folder. When you “activate” it, your terminal’s PATH is changed so that python and pip point at this directory instead of the system-wide install. Every pip install now lands inside the box. When you deactivate, the PATH change is undone and you are back to normal. That is the whole trick: a folder and a tweaked PATH.

Below is the full workflow in one picture: create the environment, activate it, install packages, freeze the exact versions, and the directory layout that makes it all hang together.

Project B: Data ScienceProject A: Web Apppython -m venvpython -m venvActivation Changes PATHsource.venv/bin/activateBefore: python/usr/bin/python3After: python.venv/bin/pythonSystem Python 3.14/usr/bin/python3Global site-packagespip, setuptoolspython -m venv .venvsite-packages:requests 2.31flask 3.0sqlalchemy 2.0python -m venv .venvsite-packages:requests 2.28pandas 2.2numpy 1.26 Without venv:requests 2.31 and 2.28can’t coexist!Python Virtual Environments: How venv Isolates Each Project’s Packages

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

Read the diagram top to bottom. One system Python sits at the top. Each project runs python -m venv .venv to carve out its own box, so the web app can keep requests 2.31 while the data science project keeps requests 2.28, and they never argue. The red box on the right shows the alternative: skip the venv, dump everything into the shared global folder, and those two requests versions cannot coexist. This is exactly why almost every Python README, tutorial, and deployment guide opens with the same line, “first, create a virtual environment.”

Creating and Activating a Virtual Environment

Activating a venv works like swapping the SIM card in your phone. Same phone, but every call now goes through a different number. Same terminal, but every python and pip command now routes through the project’s private copy. Two commands get you there: one to create the environment, one to activate it.

📄 Terminal: create and activate a venv

# Create a virtual environment in a folder named .venv
python -m venv .venv

# Activate it (Linux/macOS)
source .venv/bin/activate

# Activate it (Windows, PowerShell)
.venv\Scripts\Activate.ps1

# Verify: this should point to .venv's Python, not the system one
which python     # Linux/macOS
where python     # Windows

# Deactivate when you are done for the day
deactivate

▶ Output (after activation)

(.venv) $ which python
/home/rahul/my-project/.venv/bin/python

(.venv) $ python --version
Python 3.14.6

(.venv) $ pip list
Package Version
------- -------
pip     26.1.2

What happened here: python -m venv .venv built a folder called .venv holding a Python launcher, pip, and an empty site-packages directory. Activating it slipped .venv/bin to the front of your PATH, so now python and pip resolve to the copies inside the box. Notice that pip list shows only one package right after creation. That is the point. A fresh venv starts clean, with nothing borrowed from your system Python. (On Python 3.14.6 the venv ships pip alone; setuptools is no longer bundled, since modern pyproject.toml builds pull it in only when a package actually needs it.)

Quick Win: Prove the Isolation in 30 Seconds

Talk is cheap. Let us prove that a package installed inside the venv really does land inside the box and nowhere else. Activate the environment, install one library, then ask Python where it came from.

📄 Terminal: install one package and trace it

(.venv) $ pip install requests

(.venv) $ python -c "from importlib.metadata import version; import requests; print('requests', version('requests')); print('lives in:', requests.__file__)"

▶ Output

requests 2.34.2
lives in: /home/rahul/my-project/.venv/lib/python3.14/site-packages/requests/__init__.py

What happened here: the install path points straight into .venv/lib/.../site-packages/, not into your system Python. That one line is the whole promise of a Python virtual environment made visible. Delete the .venv folder and this copy of requests vanishes with it, while your system Python stays exactly as it was. Nothing leaks out, nothing leaks in.

What’s Inside a Virtual Environment

Open the .venv folder and you will find it is surprisingly small, like a hotel room rather than a whole house. It does not carry its own copy of Python’s source, just a key card (pyvenv.cfg) that says which building it belongs to, plus an empty shelf (site-packages/) for your own things. Here is the layout.

📄 Directory structure of .venv/

.venv/
├── bin/                  # (Scripts/ on Windows)
│   ├── python            # links back to your real Python
│   ├── pip               # pip for this environment
│   └── activate          # shell script that tweaks PATH
├── lib/
│   └── python3.14/
│       └── site-packages/  # YOUR packages go here
├── include/              # C headers for compiling extensions
├── .gitignore            # auto-created, contains *  (git skips the whole venv)
└── pyvenv.cfg            # config: home, version, system-packages

What happened here: the two pieces that matter most are site-packages/ and pyvenv.cfg. Everything you pip install drops into site-packages/, and that is the box we keep talking about. The tiny pyvenv.cfg file is how the venv finds its way home. Open it and you will see three lines that matter: home (the path to the real Python this venv was built from), version = 3.14.6, and include-system-site-packages = false. That last flag is why your system packages stay invisible inside the venv. The venv has no Python source code of its own. It just points back to the original install and keeps its own packages separate.

Managing Dependencies with requirements.txt

A requirements.txt file is a shopping list with exact brands and quantities written down. Hand it to anyone and they come back with the same basket you had, not “some flour, roughly.” pip freeze writes that list for you, and pip install -r shops from it on any machine.

📄 Terminal: install packages and freeze them

# Install the packages your project needs
pip install requests flask sqlalchemy

# Freeze the exact installed versions into requirements.txt
pip freeze > requirements.txt

# Later, on another machine or a fresh clone, rebuild the same setup
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

📄 requirements.txt: exact versions, pinned for a reproducible build

blinker==1.9.0
certifi==2026.6.17
charset-normalizer==3.4.7
click==8.4.1
colorama==0.4.6
Flask==3.1.3
greenlet==3.5.2
idna==3.18
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
requests==2.34.2
SQLAlchemy==2.0.51
typing_extensions==4.15.0
urllib3==2.7.0
Werkzeug==3.1.8

What happened here: you asked for three packages, but requirements.txt lists sixteen. That is normal and it is the whole point of freezing. flask quietly drags in Jinja2, Werkzeug, blinker, itsdangerous, and friends. sqlalchemy brings greenlet. requests brings certifi, idna, urllib3, and charset-normalizer. pip freeze records every single one with an exact == version, so when a new teammate, say Anvi, clones your repo six months from now, she gets the identical set, not whatever happens to be newest that week. That is the difference between “it works on my machine” and “it works on every machine.”

Best Practices

  • Always use a venv. Even for small scripts. It takes 5 seconds and prevents hours of debugging.
  • Name it .venv (with the dot). Most .gitignore templates already exclude it.
  • Never commit the venv directory. Add .venv/ to .gitignore. Commit requirements.txt instead.
  • Pin your versions. pip freeze > requirements.txt locks exact versions for reproducibility.
  • Recreate, don’t repair. If a venv gets corrupted, delete it and create a fresh one.

Common Mistakes

❌ Mistake 1: Installing globally by accident

# BAD: forgot to activate the venv first
pip install requests    # this lands in your global Python!

# GOOD: check your prompt shows (.venv) before you install anything
source .venv/bin/activate
pip install requests    # this lands in .venv/lib/.../site-packages/

❌ Mistake 2: Committing the venv to git

# BAD: .venv is hundreds of MB of machine-specific binaries
git add .venv/   # please do not

# GOOD: your .gitignore should contain this one line
# .venv/
# then commit requirements.txt, which is tiny and portable

Why this matters: the first mistake is the one everyone makes at least once. You open a terminal, run pip install out of habit, and the package quietly goes into your system Python instead of the project. Months later you cannot figure out why a coworker’s machine behaves differently. The fix is a reflex: glance at your prompt, and if you do not see (.venv), activate first. The second mistake bloats your repo and your pull requests with megabytes of binaries that are useless on anyone else’s operating system. Commit the recipe (requirements.txt), never the kitchen (.venv/).

Try It Yourself

Run this end to end in a scratch folder. It takes about two minutes and locks in everything above.

  1. Spin up two boxes. Make two folders, scraper and api-client, and create a .venv in each with python -m venv .venv. Activate the first and run pip list. Confirm it shows only pip, proving the box starts empty.
  2. Install different versions and prove they do not collide. In scraper run pip install "requests==2.28.2"; in api-client run pip install requests for the latest. Activate each in turn and run python -c "from importlib.metadata import version; print(version('requests'))". The two folders report two different versions. The kitchen analogy, made real.
  3. Freeze, delete, rebuild. In one project run pip freeze > requirements.txt, then delete the whole .venv folder. Recreate it with python -m venv .venv, activate, and run pip install -r requirements.txt. You just reproduced your exact environment from a single text file. That is the skill that makes your projects survive a new laptop.

Wrapping Up

You now know the full python virtual environment loop: create with python -m venv .venv, activate so PATH points inside the box, install what the project needs, and freeze exact versions into requirements.txt so anyone can rebuild the same setup anywhere. You also know what a venv really is under the hood, just a folder with a pyvenv.cfg pointing home and its own site-packages, which is why “delete and recreate” is always a safe move. Next up we compare the tools that automate all of this: pip vs Poetry vs uv vs conda. And if you want to jump to any other topic, browse the full Python + AI/ML tutorial series home.

Frequently Asked Questions

What is a Python venv?

A Python venv is a virtual environment: an isolated copy of Python with its own site-packages directory. Packages you install in a venv do not touch your system Python or any other project. You create one with python -m venv .venv and activate it before installing anything.

What does activating a virtual environment do?

Activation puts the venv’s bin/ (or Scripts/ on Windows) folder at the front of your shell’s PATH. After that, python and pip resolve to the venv’s copies instead of the system-wide ones, so every install goes into the venv.

Should I commit the virtual environment to git?

No. Never commit the .venv/ directory, because it is large and tied to one operating system. Add .venv/ to .gitignore and commit requirements.txt instead. On Python 3.14.6 the venv even auto-creates its own .gitignore so git skips it by default. Teammates recreate the venv from requirements.txt.

What is the difference between venv and virtualenv?

venv ships inside Python 3.3 and later, so there is nothing to install. virtualenv is a separate package that is a bit faster and still supports very old Python versions. For Python 3.6 and up, plain venv is enough for almost everyone. Tools like uv (covered in the next post) go further and manage both Python versions and dependencies.

Can I use multiple Python versions with venv?

Each venv is locked to the Python version that built it. To work in Python 3.13 and 3.14 side by side, create separate venvs: python3.13 -m venv .venv313 and python3.14 -m venv .venv314. Tools like pyenv (Linux/macOS) or the py launcher (Windows) help you keep several Python versions installed at once.

Interview Questions on Python Virtual Environments

Scenario questions, not trivia: this is the form this topic takes in a real interview.

Q: You activated your venv, ran pip install requests, and the install succeeded, yet python -c "import requests" throws ModuleNotFoundError. What do you check first?

Check whether python and pip are resolving to the same interpreter: run which python and pip --version (which prints the path pip is installed for). A mismatch usually means pip came from the system install or another venv earlier in PATH, so the package landed outside your active environment. The bulletproof fix is to install with python -m pip install requests, which guarantees pip runs under the exact interpreter you will import with. Also confirm your IDE is pointed at .venv/bin/python, since editors keep their own interpreter setting separate from the terminal.

Q: Your app works on your laptop but crashes on the production server with an AttributeError coming from a third-party library. Both machines “installed the requirements.” Where do you look?

Compare pip freeze output on both machines. The usual culprit is an unpinned or loosely pinned requirements file (flask instead of Flask==3.1.3), so the server pulled a newer release with a changed or removed attribute. Regenerate a fully pinned requirements.txt from the working environment, rebuild the server’s venv from scratch with pip install -r requirements.txt, and the two machines converge. This is exactly why pinning exact versions matters for deployments.

Q: A teammate insists activation is what “installs” the venv’s packages. What does activation actually change, and could you use the venv without ever activating it?

Activation only edits your shell session: it puts the venv’s bin/ (or Scripts/ on Windows) at the front of PATH and adds the (.venv) prefix to the prompt. Nothing is installed or modified on disk. And yes, you can skip it entirely by calling the interpreter by its full path, like .venv/bin/python app.py or .venv/bin/pip install requests. Scripts, cron jobs, and CI pipelines commonly do exactly this because activation is a convenience for humans, not a requirement.

Q: You upgraded or reinstalled your system Python, and now an old project’s venv fails to start. Why, and what is the fix?

A venv has no Python of its own. Its pyvenv.cfg stores a home path back to the interpreter that created it, and on Linux/macOS the bin/python is typically a symlink to that install. Move, upgrade, or delete the original Python and that link dangles, so the venv breaks. The fix is the standard rule from this post: do not repair, recreate. Delete .venv, run python -m venv .venv with the new interpreter, and reinstall from requirements.txt.

Q: Why does pip freeze list far more packages than you installed, and is that a problem?

It lists transitive dependencies: install Flask and you also get Jinja2, Werkzeug, click, and friends, because Flask needs them to run. Recording all of them with exact == versions is a feature, not noise, since an unpinned transitive dependency can break your build just as easily as a direct one. The trade-off is that a raw freeze mixes direct and indirect packages together, which is why larger projects keep a short hand-written list of direct dependencies and generate a fully pinned lock file from it.

Q: When would you pass --system-site-packages to python -m venv, and what is the risk?

That flag flips include-system-site-packages to true in pyvenv.cfg, letting the venv see globally installed packages as a fallback while still installing new ones locally. It is occasionally useful when a huge, hard-to-build package already exists system-wide and you do not want to reinstall it. The risk is that you lose true isolation: your project silently depends on global packages that pip freeze inside the venv will not fully capture, so the environment stops being reproducible. Default to full isolation unless you have a concrete reason not to.

Reference: the complete, always-current details live in Python venv documentation.

Previous: Python: 20 Most Common Error Messages, What They Mean & How to Fix

Next: Python: Debugging & Basic Testing with pdb, breakpoint(), assert

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *