01 · THE MODEL
A virtual environment is a directory and a path trick.
There is no magic here, and knowing the mechanism makes the failures obvious instead of mysterious.
A virtual environment is a directory containing a link to a Python interpreter, its own site-packages folder, and scripts that put its bin or Scripts directory first on your PATH. Activating it means adjusting PATH for that shell. Nothing more.
Which explains the classic failures:
- “I installed it but Python cannot import it” — the install went to a different environment than the interpreter you ran.
- “It works in the terminal but not in the editor” — the editor selected a different interpreter.
- “It broke after I installed something unrelated” — a shared global environment where one package upgraded another package’s dependency.
- “sudo pip install fixed it” — it did not. It modified the system Python that your operating system depends on.
Never install into the system Python. On Linux and macOS, package managers and OS tooling depend on it. Modern Python even refuses by default, and the correct response to that error is to make an environment, not to override the protection.
02 · THE BASELINE
venv ships with Python and is enough for many projects.
# Create an environment in the project directory
py -3.12 -m venv .venv
# Activate it for this shell
.\.venv\Scripts\Activate.ps1
# Install into it, then record what you installed
pip install requests
pip freeze > requirements.txt
deactivate
python3 -m venv .venv
source .venv/bin/activate
pip install requests
pip freeze > requirements.txt
deactivate
Conventions that prevent most confusion
- Name it
.venv, inside the project. Editors detect that name automatically, and it keeps the environment next to what it belongs to. - Add it to
.gitignore. Environments are built artefacts, not source. They are also not portable between machines. - Use
python -m piprather than barepipwhen in doubt. It guarantees the install goes to the interpreter you think it does. - Select the interpreter in your editor explicitly, and check the status bar rather than assuming.
Where venv stops being enough: requirements.txt from pip freeze records what you happened to have, not what you asked for. It mixes direct dependencies with transitive ones, does not capture hashes, and does not distinguish development tools from runtime requirements. That is the gap the next section fills.
03 · THE TOOLS
Five tools, five different problems.
| Tool | Solves | Use it when |
|---|---|---|
| venv + pip | Isolation | Small projects, scripts, and anywhere you want zero extra tooling |
| uv | Isolation, resolution, locking, Python version installs — quickly | Most new projects. It replaces several tools and is dramatically faster. |
| Poetry | Dependency management and packaging with a lockfile | Libraries you publish, and teams that want one opinionated workflow |
| pipx | Installing command line applications in isolation | Anything you run rather than import: linters, formatters, generators |
| conda / mamba | Non-Python binary dependencies and scientific stacks | Data and scientific work where compiled libraries are the hard part |
A default worth adopting
# Start a project with a pyproject.toml
uv init my-project
cd my-project
# Add dependencies; the lockfile updates automatically
uv add requests
uv add --dev pytest ruff
# Run inside the environment without activating it
uv run pytest
# Reproduce the exact environment elsewhere
uv sync
The important part is not the speed, it is uv.lock: an exact, hashed resolution of every package that uv sync reproduces on another machine. That is the difference between a project that rebuilds and one that merely usually rebuilds.
pipx for tools, always
pipx install ruff
pipx install httpie
pipx list
# Or run something once without installing it
pipx run cowsay hello
Each tool gets its own environment and a shim on PATH, so installing two applications with conflicting dependencies stops being a problem. If you have ever broken a linter by installing an unrelated CLI, this is the fix.
04 · INTERPRETERS
The version of Python is part of the environment.
An environment pins packages. It does not pin the interpreter that created it, which is the second half of reproducibility.
- Windows: use the
pylauncher.py -3.12 -m venv .venvselects the interpreter explicitly rather than relying on PATH order. - uv can install interpreters —
uv python install 3.12— which removes the need for a separate version manager entirely. - pyenv and mise manage multiple interpreters on Unix-like systems; pyenv-win exists but is rougher than the alternatives.
- Commit a
.python-versionfile so the next person, and CI, resolve the same interpreter. - Declare
requires-pythoninpyproject.tomlso resolution fails loudly rather than installing something incompatible.
Do not delete the system Python to install a newer one. On Linux, package management depends on it. Install additional versions alongside and select them per project — the same principle as Node version managers, for the same reason.
05 · THE SCIENTIFIC CASE
conda solves a different problem, and mixing is where it hurts.
conda manages non-Python binaries — compilers, CUDA runtimes, linear algebra libraries — which is exactly the part pip historically could not handle.
Wheels have narrowed that gap considerably, so plain pip now works for many scientific stacks. conda still wins when you need specific compiled toolchains, a pinned CUDA runtime, or a stack that is genuinely painful to build from source.
- Use mamba, or conda’s modern solver. The classic solver is slow enough to change how you work.
- Prefer conda-forge and be consistent about channels. Mixing channels is where unexplainable conflicts come from.
- Do not mix pip and conda casually. If you must, install everything available through conda first, then pip for the remainder, and never install a package with both.
- Export properly.
conda env export --from-historyrecords what you asked for, not the entire resolved graph including platform-specific builds.
If nothing in your project needs a compiled toolchain, you do not need conda. Many people carry it for years out of habit from one tutorial, and pay for it in resolution time and channel conflicts.
06 · REPRODUCIBILITY
Six habits that make the environment rebuildable.
uv.lock, poetry.lock or a compiled requirements file with hashes. Without it, “install the dependencies” is a different operation every week.
Declare what you actually import in pyproject.toml. Let the lockfile hold the rest.
Test and lint tools do not belong in a production install. Every modern tool supports a dev group.
.python-version plus requires-python. An environment built on a different Python is a different environment.
Delete .venv and reinstall from the lockfile. It is the only way to find the dependency you installed manually and never declared.
Three lines in the README beat any tooling. Someone, including future you, will arrive without context.
The test that matters: clone the repository into a new directory on a machine that has never seen it, run your documented setup command, and run the test suite. If that fails, the environment is not reproducible regardless of which tool produced it.
07 · QUICK ANSWERS
Python environments, briefly.
A directory containing a link to a Python interpreter, its own site-packages folder and activation scripts that put it first on your PATH. Activating one adjusts PATH for that shell so installs and imports resolve to that environment rather than to the system Python.
venv and pip are enough for small projects and require nothing extra. uv is the best default for new projects: it creates environments, resolves and locks dependencies, and can install Python versions, all considerably faster. Poetry suits libraries you publish and teams that want one opinionated workflow. All three are fine if you commit a lockfile.
Only if you need non-Python binary dependencies such as specific compiled toolchains or a pinned CUDA runtime. Wheels have made plain pip viable for most scientific stacks. If nothing in your project needs a compiler, conda adds resolution time and channel conflicts without solving a problem you have.
The install and the import used different interpreters. Check which environment is active, run python -m pip install rather than bare pip so the install targets the interpreter you are running, and confirm your editor has selected the same interpreter as your terminal.
No. Environments are built artefacts, contain absolute paths and platform-specific binaries, and are not portable between machines. Add .venv to .gitignore and commit the lockfile instead, which is what allows the environment to be rebuilt identically.