Setting Up Python Virtual Environments the Way I Wish Someone Had Shown Me Early On

By James Nguyen Updated September 24, 2026
Setting Up Python Virtual Environments the Way I Wish Someone Had Shown Me Early On

I installed packages globally with pip for my first two years writing Python, and it worked fine right up until I had two side projects that needed different, incompatible versions of the same library installed at the same time. Running one project broke the other, and I spent an entire evening reinstalling packages back and forth before finally sitting down to actually learn virtual environments properly instead of working around the problem.

What a virtual environment actually solves, stated plainly

A virtual environment is an isolated Python installation living in its own folder, with its own separate set of installed packages, meaning two projects can each have their own version of the same library without ever conflicting, exactly the problem that cost me that entire evening of reinstalling things by hand.

Creating one with the built-in venv module, no extra tools needed

Python ships with a built-in module for this, no separate installation required, and creating one is a single command run inside a project's folder.

python3 -m venv .venv
source .venv/bin/activate   # on macOS or Linux
.venv\Scripts\activate      # on Windows

Once activated, your shell prompt changes to show the environment's name, and any pip install command from that point only affects this one isolated folder rather than your entire system's Python installation.

The mistake that cost me a confusing afternoon early on

I once ran pip install requests from a terminal tab where I'd forgotten to activate the environment, installing it globally instead, and spent a genuinely confusing hour wondering why my project's requirements file didn't match what was actually available when a teammate tried to run it. I now always check my prompt for the environment name before running any install command, a habit that's prevented several repeats of that exact mistake since.

Freezing dependencies into a requirements file, and why the raw output needs editing

Running pip freeze > requirements.txt captures every installed package and its exact version, but it also captures every transitive dependency your direct packages pulled in, producing a genuinely bloated file that's hard to read and prone to unnecessary version conflicts down the line.

pip freeze > requirements.txt

I now maintain a separate, hand-written list of just my direct dependencies, and generate the full frozen file only for actual deployment, keeping the two genuinely separate rather than treating the raw freeze output as my source of truth.

Why I eventually switched to Poetry for anything beyond a quick script

For a genuinely small script, venv and a manual requirements file remain perfectly fine, but for any real project with a team, I switched to Poetry, which manages the virtual environment, dependency resolution, and a proper lock file automatically rather than requiring the manual discipline venv alone demands.

poetry init
poetry add requests
poetry install

The lock file specifically solved a real problem, a teammate installing slightly different transitive dependency versions than what I'd tested against, something the raw pip freeze approach never fully guaranteed against.

A specific gotcha with system Python versions that tripped me up

On one machine, running python3 -m venv silently used an outdated system Python I didn't realize was still installed alongside a newer version I'd meant to use, producing a virtual environment locked to the wrong interpreter version entirely. I now explicitly specify the interpreter path when creating an environment rather than trusting whichever python3 happens to resolve first on a given machine.

python3.12 -m venv .venv

Keeping the .venv folder out of version control

The virtual environment folder itself should never be committed, it's large, machine-specific, and entirely reproducible from the requirements file alone, so I add it to .gitignore in every project from the very first commit rather than discovering a bloated repository history after the fact the way I did on one early project.

Managing multiple Python versions with pyenv, once one system version wasn't enough

The gotcha above, a venv silently built against the wrong system interpreter, eventually pushed me to install pyenv specifically to manage multiple Python versions cleanly side by side, rather than relying on whatever happened to be installed system-wide.

pyenv install 3.12.4
pyenv local 3.12.4
python3 -m venv .venv

Setting a local version this way writes a small file to the project folder that automatically selects the right interpreter whenever I'm working inside it, removing the guesswork entirely rather than relying on my own memory to pick the right version by hand every time.

Splitting dev and production dependencies into separate files

My requirements file originally listed everything together, testing libraries and linters alongside actual runtime dependencies, meaning production deployments installed genuinely unnecessary packages every time. Splitting into a base requirements.txt and a separate requirements-dev.txt that references the base file kept production installs lean while still giving my own local environment everything it needs for testing and linting.

# requirements-dev.txt
-r requirements.txt
pytest
black
ruff

What I'd tell someone still installing packages globally

That evening of reinstalling conflicting packages by hand was a genuinely avoidable mistake, and virtual environments solve it completely once the habit of activating one before any install actually sticks. It felt like unnecessary ceremony for a single script back when I first learned it, and it took exactly one real dependency conflict to convince me it's actually the opposite, a small habit that prevents a genuinely much worse afternoon down the line.

Daniel Justin

About the Author

James Nguyen is a full-stack programmer with more than ten years of experience engineering software systems. Specializing in the Node.js and Python ecosystems, he focuses on backend architecture, API design, and clean data integration. Follow me on YouTube and Instagram.

More Articles