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.

POWERSHELL · VENV ON WINDOWS
# 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
BASH · THE SAME THING ON LINUX OR MACOS
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 pip rather than bare pip when 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.

ToolSolvesUse it when
venv + pipIsolationSmall projects, scripts, and anywhere you want zero extra tooling
uvIsolation, resolution, locking, Python version installs — quicklyMost new projects. It replaces several tools and is dramatically faster.
PoetryDependency management and packaging with a lockfileLibraries you publish, and teams that want one opinionated workflow
pipxInstalling command line applications in isolationAnything you run rather than import: linters, formatters, generators
conda / mambaNon-Python binary dependencies and scientific stacksData and scientific work where compiled libraries are the hard part

A default worth adopting

SHELL · UV, END TO END
# 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

SHELL · COMMAND LINE TOOLS
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 py launcher. py -3.12 -m venv .venv selects the interpreter explicitly rather than relying on PATH order.
  • uv can install interpretersuv 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-version file so the next person, and CI, resolve the same interpreter.
  • Declare requires-python in pyproject.toml so 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-history records 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.

HABIT—01 Commit the lockfile

uv.lock, poetry.lock or a compiled requirements file with hashes. Without it, “install the dependencies” is a different operation every week.

HABIT—02 Separate direct from transitive

Declare what you actually import in pyproject.toml. Let the lockfile hold the rest.

HABIT—03 Split dev dependencies

Test and lint tools do not belong in a production install. Every modern tool supports a dev group.

HABIT—04 Pin the interpreter

.python-version plus requires-python. An environment built on a different Python is a different environment.

HABIT—05 Rebuild from scratch periodically

Delete .venv and reinstall from the lockfile. It is the only way to find the dependency you installed manually and never declared.

HABIT—06 Write down the setup

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.

What is a Python virtual environment?

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.

Should I use venv, Poetry or uv?

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.

Do I need conda?

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.

Why does pip install work but the import still fail?

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.

Should I commit the virtual environment to Git?

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.