You clone a teammate's Python project, run pip install -r requirements.txt, and get a wall of red text. Two hours later you discover the project wanted Python 3.11, your machine has 3.9, and a package you installed last month for a different project quietly upgraded something both of them share. Nothing is "broken" in the usual sense. Your setup is simply a pile of leftovers.
This is the problem virtual environments exist to solve, and in the last couple of years a newer tool called uv has changed how many people solve it. Let's walk through what an environment actually is, the classic venv + pip workflow, and where uv fits in, so you can pick one on purpose rather than by habit.
What a virtual environment really is
A virtual environment is just a folder. Inside it lives a private copy of (or link to) a Python interpreter plus a site-packages directory where installed libraries go. When you "activate" it, your shell puts that folder's bin (or Scripts on Windows) at the front of your PATH, so python and pip now mean this project's versions.
An environment isn't magic. It's a folder and a PATH change, which is exactly why it's safe to delete and recreate.
That mental model removes most of the fear. If an environment gets weird, you don't debug it for an hour: you delete the folder and rebuild it from your dependency list. The whole game is keeping that dependency list honest.
The classic workflow: venv and pip
The standard library ships with venv, so this works on any machine with Python installed:
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests flask
pip freeze > requirements.txtThis is reliable and everywhere. Its weaknesses show up as projects grow. pip freeze dumps every installed package, including the dependencies of your dependencies, so the file mixes what you chose with what came along for the ride. Removing a library later leaves its orphaned helpers behind. And pip installs one thing at a time in a fairly simple way, so big environments can feel slow.
There is also the "which Python?" question. venv uses whichever interpreter you ran it with. If you need 3.12 and only have 3.9, you have to install 3.12 yourself first, through your OS package manager, pyenv, or an installer.
Where uv comes in
uv is a Python package and project manager written in Rust and developed by Astral. Its pitch is simple: one fast tool that covers creating environments, installing packages, pinning versions, and even fetching Python itself. As of writing, it is actively developed and widely adopted, but check the official docs for the current command set before you script against it.
Here is the same kind of setup with uv's project workflow:
uv init myapp
cd myapp
uv add requests flask
uv run python main.pyNotice what you did not do: you never created or activated an environment by hand. uv add records the dependency in pyproject.toml, creates a .venv for you if needed, installs the package, and writes a lock file (uv.lock) that pins exact versions. uv run executes your command inside that environment without an activation step.
For people who don't want to change how they work yet, uv also offers a drop-in style interface:
uv venv
uv pip install requestsThat gives you a familiar shape with much of the speed benefit. Speed claims vary by project, so measure on your own machine rather than trusting anyone's benchmark, mine included.
Direct comparison
| Question | venv + pip | uv |
|---|---|---|
| Ships with Python | Yes | No, separate install |
| Creates the environment | python -m venv | Automatic in project mode |
| Records only what you chose | Not by default | Yes, in pyproject.toml |
| Exact version lock file | Needs extra tooling | Built in (uv.lock) |
| Can install a Python version | No | Yes |
| Learning curve | Lowest | Small, but new commands |
The table hides one honest tradeoff: venv is the lowest common denominator. Every CI server, tutorial, and Docker image assumes it. uv is a newer tool, so you're adding a dependency to your tooling itself.
A practical example: reproducing a project
Imagine a teammate joins your project. With venv and a well-kept requirements.txt, they run three commands and hope their Python version matches. With a uv project, the flow is:
git clone <your-repo>
cd your-repo
uv sync
uv run pytestuv sync reads the lock file and builds the environment to match it exactly. The lock file is the real prize here. It answers "what versions did this work with?" in a way a hand-edited requirements list rarely does, and it turns "works on my machine" into something you can actually check.
You can still export a classic file when a platform demands one, for example a deployment target that only understands requirements.txt. Check uv's documentation for the export options available in your version.
So which should you use?
A few rules of thumb help:
- Learning Python, or a tiny script: stick with
venvandpip. Fewer moving parts, and every tutorial matches what you see. - A project with several dependencies or teammates: try uv. The lock file and automatic environment handling remove whole categories of "it worked yesterday" problems.
- An existing team on
venv,pip-tools, or Poetry: don't migrate for fashion. Switch only if you're actually feeling pain, and try it on one small project first. - Anything you deploy: whichever tool you choose, commit the lock or pinned file and build from it, so production matches what you tested.
Whatever you pick, two habits matter more than the tool: one environment per project, and never install project libraries into your system Python. Add .venv/ to .gitignore, and commit your dependency files instead of the environment itself.
Wrapping up
A virtual environment is a disposable folder that keeps one project's libraries from tangling with another's. The classic venv + pip route is universal and dependable. uv builds on the same idea but adds automatic environments, exact lock files, and the ability to manage Python versions, all from one command-line tool.
Try it on a throwaway project this weekend: run uv init, add one package, and delete the .venv folder just to watch uv sync bring it back. Once you've seen that rebuild take seconds, dependency drama feels a lot less scary.


