Python Virtual Environments — Django 1.11 & 4.0 Conflict
One server running Django 1.11 and 4.0 causes AttributeError crashes.
20+ years shipping production Python across data and backend systems. Drawn from code that ran under real load.
- A virtual environment is an isolated folder with its own Python binary and packages — nothing affects the global Python installation.
- Create with
python -m venv .venv; activate withsource .venv/bin/activate(Mac/Linux) or.venv\Scripts\activate(Windows). - When active, your terminal prompt shows
(.venv)— that's your visual confirmation isolation is working. pip freeze > requirements.txtcaptures exact package versions;pip install -r requirements.txtrecreates the environment anywhere.- Performance insight: Virtual environments add zero runtime overhead — they just change which Python binary your terminal uses.
- Production failure: committing the
.venvfolder to Git causes repository bloat (100+ MB) and broken setups across operating systems. - Biggest mistake: forgetting to activate before running
pip install— packages go into the global environment and your project fails on another machine.
Imagine you're a chef who cooks at two different restaurants. Restaurant A wants you to use only olive oil; Restaurant B insists on butter. You can't mix them up or both meals are ruined. A Python virtual environment is your own private kitchen for each project — it keeps that project's ingredients (libraries) completely separate from every other project's ingredients, so nothing ever gets mixed up or breaks each other.
You clone a repo, run pip install, and suddenly your Django 3 project breaks because your system has Django 5. That’s what happens without virtual environments. They isolate project dependencies so each app gets exactly the packages it needs, and your global Python stays clean and stable.
Why Python Virtual Environments Are Non-Negotiable
A Python virtual environment is an isolated directory that contains its own Python interpreter, site-packages, and scripts. The core mechanic: it modifies the sys.path so that import statements resolve to packages installed inside that environment, not the global system Python. This prevents the classic 'Django 1.11 vs 4.0' conflict where two projects on the same machine require different major versions of the same library.
Each environment is a self-contained copy of the Python binary plus a lib/pythonX.Y/site-packages/ folder. When you activate it, your shell's PATH is prepended with the environment's bin/ directory, making python and pip point to the local versions. Deactivation restores the original PATH. No magic — just careful path manipulation.
Use a virtual environment for every project, without exception. In production, you'll typically recreate environments from a requirements.txt or Pipfile.lock inside a Docker container. The rule: one environment per project, never share between projects, and never install project dependencies globally. This eliminates version conflicts and makes builds reproducible.
pip install after activation.TemplateDoesNotExist errors for valid templates, or ImportError for new Django features.pip list inside the active environment to verify the installed versions match your lock file.Why Global Package Installation Is a Ticking Time Bomb
When you first install Python and run pip install requests, that library lands in a single global location on your computer. Every Python script you ever write shares that same copy. Sounds convenient — until it isn't.
Picture this: your first project uses requests version 2.20. Six months later you start a new project that needs requests version 2.31 because it uses a feature that didn't exist before. You upgrade globally. Your old project breaks. You downgrade to fix the old project. The new project breaks. You're stuck in a loop with no clean way out.
This exact scenario has a name in software: dependency conflict. It's not hypothetical — it's the single most common source of pain for Python beginners and professionals alike.
Virtual environments break this cycle permanently. Each environment is a self-contained folder that holds its own Python interpreter and its own set of packages. Project A's requests 2.20 and Project B's requests 2.31 can coexist peacefully on the same machine because they live in completely different folders and never meet.
Here's a subtle but important detail: virtual environments don't copy the entire Python installation. They create symlinks (Mac/Linux) or shortcuts (Windows) to the Python binary and then add their own site-packages folder. This keeps environments lightweight — typically 10-20 MB plus whatever packages you install.
/usr/local/bin/python3. When you activate a virtual environment later, this path will change to something inside your project folder — that's your proof the isolation is working.sudo pip install on the global Python. A security update to a system package required a new version of requests—and their app broke at 3 AM.pip install away from breaking something else.Creating and Activating Your First Virtual Environment
Python ships with a built-in module called venv (short for virtual environment). You don't need to install anything — it's already there, waiting. You create a virtual environment with a single terminal command, and from that moment on, that folder is your project's private kitchen.
The general pattern is: python -m venv <name-of-environment>. The name is just a folder name — most developers use venv or .venv (with a dot prefix so the folder is hidden by default on Mac/Linux).
Once created, you need to activate it. Activating tells your terminal 'for every command I run from now on, use the Python and pip inside this folder, not the global ones'. The activation command is slightly different depending on your operating system, but the effect is identical.
After activation your terminal prompt usually changes to show the environment name in parentheses — that's your visual confirmation that you're inside the bubble. When you're done working, deactivate drops you back to the global environment.
What's actually happening when you activate? The activation script modifies your shell's PATH environment variable, adding the .venv/bin directory to the front. Since shells search PATH in order, python now resolves to .venv/bin/python first. The VIRTUAL_ENV environment variable is also set so tools can detect which environment is active. The deactivate command restores the original PATH.
.venv\Scripts\Activate.ps1 might throw 'running scripts is disabled on this system'. This is a Windows security setting — it doesn't affect your code at all. Fix it by running Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser in PowerShell once. Alternatively, use Command Prompt where .venv\Scripts\activate.bat works without policy changes.pip install -r requirements.txt in the global Python, then python -m pytest also runs globally — but the packages are installed in the global site-packages. Everything passes locally because your environment was activated, but fails in CI.source .venv/bin/activate && python -m pytest or use python -m venv --clear to ensure a fresh environment per run.python -m venv .venvsource .venv/bin/activate (Mac/Linux) or .venv\Scripts\activate (Windows)(.venv) — if you don't see it, you're not inside the environment.Freezing Dependencies So Your Team Gets Identical Setups
Stop relying on pip list for reproducibility. pip list shows installed packages including those from the system or editable installs — it's a mess. pip freeze outputs only packages installed via pip in the exact format pip needs to reinstall them: package==version. That's what goes into requirements.txt. Period.
But pinning everything with == creates the pinning problem: your requirements.txt locks every transitive dependency, making upgrades a nightmare. You don't know which deps are direct vs indirect. Enter pip-compile from pip-tools. Create a requirements.in file listing only your direct dependencies (e.g., requests>=2.31.0). Run pip-compile requirements.in to generate requirements.txt with all transitive deps pinned. Devs edit .in, CI uses .txt. Workflow: pip-compile --upgrade-package requests to bump just one dep without blowing up the lockfile.
For supply chain security, install with hash verification: pip install -r requirements.txt --require-hashes. Generate hashes via pip-compile --generate-hashes requirements.in. This ensures every downloaded wheel matches a known SHA-256.
Dockerfile pattern for layer caching: `` COPY requirements.txt . RUN pip install -r requirements.txt --require-hashes COPY . . ` This caches the dependency layer until requirements.txt` changes, slashing rebuild times.
pip list --format=freeze and got system packages like apt or setuptools pinned. That breaks on different base images. Always use pip freeze from a clean virtualenv.requirements.in for direct deps, pip-compile for lockfile, --require-hashes for integrity, and Docker layer caching to keep builds fast.A Real-World Mini Project Tying It All Together
Theory is fine, but let's build something real inside a virtual environment so the whole workflow clicks. We'll write a small script that fetches the current weather for a city using the requests library — a package we install inside a virtual environment, not globally.
This example shows the complete developer workflow you'll repeat on every Python project you ever build: create environment → activate → install dependencies → write code → freeze requirements → deactivate. Once this muscle memory kicks in, you'll do it automatically.
Pay attention to the Python script itself too. It runs perfectly inside the virtual environment because requests is installed there. If you deactivate the environment and try to run the same script with the global Python, it would fail with ModuleNotFoundError: No module named 'requests' — unless you also happen to have it globally. That's virtual environment isolation working exactly as intended.
The script uses a free weather API (Open-Meteo) that requires no API key — perfect for learning. It demonstrates error handling for network issues and API errors, showing real production considerations even in a demo.
requests globally and share only your .py file, a colleague cloning your repo gets ModuleNotFoundError immediately. The professional fix is always: virtual environment + requirements.txt committed to the repo. Interviewers love this answer because it shows you think about reproducibility, not just making code work on your own machine.pip install --user.' The problem: --user still stores packages in a single user-wide location, just not system-wide. Two projects can still conflict.--user is for tools you want available across all projects (like black, mypy), not for project dependencies.--user for project dependencies — that's what virtual environments are for.which python — it should point inside .venv.pip install -r requirements.txt.Lock Your Environment With a Spec File, Not a Prayer
Pip freeze > requirements.txt is the bare minimum. But raw freeze is brittle. It dumps every dependency, including ones you don’t explicitly import. Your CI pipeline picks up different OS-level patches? Your environment breaks silently.
Use pip-compile from pip-tools instead. It creates a locked requirements.txt from a declarative requirements.in. That way you pin exact versions only for packages you actually need. Transitive dependencies get locked too, but you control the source.
This is what separates hobby projects from production pipelines. Locking transitive deps prevents a pandas-hotfix version from introducing an incompatible numpy build. Your teammates and your container build get exactly the same environment, down to the patch level.
Run pip-compile requirements.in once. Commit the generated requirements.txt. Never touch it manually.
Activate Once, Forget Forever: Shell Integration Saves Hours
Activating a virtual environment manually every time you open a terminal is friction. And friction kills workflow.
Ship it with direnv (or autoenv if you're legacy). Drop a .envrc file in your project root that reads:
layout python3
That’s it. Every time you cd into that directory, direnv automatically activates the venv. Leave the directory? It deactivates. No source ./venv/bin/activate. No muscle memory required.
Direnv works with pyenv too. You can set a specific Python version per directory:
use python 3.12
The shell prompt changes to show the active env. One less mental context switch. Onboarding a junior dev? They cd into the repo, and everything just works. No docs needed for setup.
Stop wasting terminal history on activation commands. Automate it.
venv vs virtualenv vs pipenv vs Poetry — Pick the Right Tool
## 1. venv (stdlib)
Zero dependencies. Ships with Python 3.3+. Use for simple scripts, CI pipelines, or when you need zero overhead.
``bash python -m venv .venv source .venv/bin/activate pip install requests ``
Weakness: No lock file, no dependency resolution beyond pip's flat list. You'll get different versions on different machines unless you manually pin everything. No build system. No publish.
When to use: One-off scripts, throwaway containers, CI where you control the environment exactly.
## 2. virtualenv
The old guard. Needed for Python 2 legacy. Has extra flags like --copies (copy instead of symlink) and --symlinks (force symlinks). Still maintained but not needed for Python 3+.
``bash pip install virtualenv virtualenv --python=python2.7 .venv source .venv/bin/activate ``
When to use: You're maintaining a Python 2 codebase in 2026. Otherwise, use venv.
## 3. pipenv
Pipfile + Pipfile.lock. Auto-manages .venv in your project directory. Was supposed to be the future, but the resolver is slow, the project had long abandonment periods, and trust is broken.
``bash pip install pipenv pipenv install requests pipenv shell ``
Current status (2026): Still maintained but not recommended for new projects. The lock file works, but the resolver can take minutes for complex graphs. Use only if you're already on it and migration cost is too high.
When to use: You're already on it and can't migrate. Otherwise, skip.
## 4. Poetry
The 2026 standard. Uses pyproject.toml, generates poetry.lock, handles build system, and can publish to PyPI. Fast resolver, deterministic builds, and first-class support for both libraries and applications.
```bash # Install curl -sSL https://install.python-poetry.org | python3 -
# New project poetry new my-project cd my-project
# Add dependency poetry add requests
# Install from lock poetry install
# Switch Python version poetry env use /usr/bin/python3.11
# Build and publish poetry build poetry publish ```
When to use: Any team project, library, or application that needs reproducibility and deployment.
## 5. Feature Comparison
| Feature | venv | virtualenv | pipenv | Poetry |
|---|---|---|---|---|
| Lock file | ❌ | ❌ | ✅ (Pipfile.lock) | ✅ (poetry.lock) |
| Dependency resolution | ❌ (pip) | ❌ (pip) | ✅ (slow) | ✅ (fast) |
| Build system | ❌ | ❌ | ❌ | ✅ |
| Publish to PyPI | ❌ | ❌ | ❌ | ✅ |
| Python version switching | ❌ | ❌ | ❌ | ✅ (poetry env use) |
| Speed | Instant | Instant | Slow | Fast |
| Zero dependencies | ✅ | ❌ | ❌ | ❌ |
## 6. Decision Rule
- Simple script →
venv - Team application →
Poetry - Maintaining legacy →
virtualenv - Nowhere →
pipenv(unless already on it, then stay until you can migrate)
pyenv + venv: Managing Multiple Python Versions Like a Pro
The Core Problem
Ubuntu 22.04 ships Python 3.10. Period. You need 3.12 for that shiny new library. System Python is frozen by apt. Don't touch it — break your OS. Enter pyenv.
pyenv Installation
``bash curl https://pyenv.run | bash ``
Add to ~/.bashrc: ``bash export PYENV_ROOT="$HOME/.pyenv" [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init -)" ``
How Shims Work
pyenv inserts $PYENV_ROOT/shims at the front of your PATH. When you type python, the shim intercepts the call, reads .python-version (or falls back to global), and routes to the correct Python binary. No aliases, no symlink chaos.
Essential Commands
```bash # Install a specific version pyenv install 3.12.3
# Set for current directory (creates .python-version) pyenv local 3.12.3
# Set global default pyenv global 3.11.9
# Verify python --version # should show 3.12.3 ```
.python-version is a plain text file containing just the version string. Commit it.
Combining pyenv with venv
``bash cd /your/project pyenv local 3.12.3 python -m venv .venv source .venv/bin/activate ``
This creates a venv using the pyenv-managed Python 3.12.3. The venv is tied to that exact interpreter. No more "but I installed 3.12" confusion.
Team Pattern
Commit .python-version to git. Add .venv/ to .gitignore. Every dev runs: ``bash pyenv install python -m venv .venv source .venv/bin/activate ``
Reproducible. Fast. No fights.
Docker Consideration
Inside Docker, skip pyenv. Use official images: ``dockerfile FROM python:3.12-slim ``
pyenv is a host-side tool. Containers are disposable — just pick the right base image. Don't install pyenv in production images.
pyenv install --skip-existing to avoid rebuilds. For Docker, skip pyenv entirely — use python:3.12-slim and pip install -r requirements.txt. pyenv is for dev machines and CI runners, not production containers..python-version, ignore .venv, and sleep better.The Production Server That Had Django 1.11 and 4.0 Side by Side
AttributeError on every request. Rolling back the deployment fixed App A but broke App B's new features. The team was stuck in a loop.pip install django==4.0 ran for App B, it overwrote the global Django 1.11. App A's code, written for 1.11, tried to use 4.0 APIs and crashed. The server had no mechanism to isolate dependencies per application.python -m venv /opt/appA/venv and python -m venv /opt/appB/venv.
2. Installed each app's dependencies inside its own environment.
3. Updated the systemd service files to point to each app's virtual environment Python: ExecStart=/opt/appA/venv/bin/gunicorn ....
4. Verified isolation by checking which python inside each service — each pointed to its own .venv/bin/python, not the global.
5. Never touched the global Python again for application dependencies.- One server, one global Python is a single point of failure for dependency conflicts.
- Every application — even on the same server — needs its own virtual environment.
- Systemd, cron, and supervisor can all use the virtual environment's Python binary directly:
/path/to/venv/bin/python script.py. - Never install application dependencies globally on a production server. Your global Python should only contain what the OS needs.
ModuleNotFoundError: No module named 'requests' despite running pip install requests(.venv). Run which python (Mac/Linux) or where python (Windows). If it points to /usr/bin/python or C:\Python3, you're in global Python. Activate the environment first.ModuleNotFoundErrorpip freeze > requirements.txt and commit the file. Your colleague should run pip install -r requirements.txt after activating their environment. Also check that .venv is in .gitignore — never commit the folder itself.(.venv) but which python still points to a system pathGet-ExecutionPolicy. Run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser. On Mac/Linux, ensure you used source .venv/bin/activate, not just .venv/bin/activate (missing source runs the script in a subshell, which doesn't affect your current shell).pip freeze shows dozens of packages you don't recognise.venv, recreate it with python -m venv .venv, activate, then install ONLY what your project needs. Run pip freeze again — it should show only the packages you explicitly installed plus their dependencies.which python # macOS/Linuxwhere python # Windows (Command Prompt)/usr/local/bin/python or C:\Python3, you are NOT in a virtual environment. Activate it with source .venv/bin/activate or .venv\Scripts\activate.Key takeaways
python -m venv .venv. Activate with source .venv/bin/activate (Mac/Linux) or .venv\Scripts\activate (Windows).(.venv) when activepip freeze > requirements.txt captures exact versions. pip install -r requirements.txt recreates the environment anywhere..venv to Git.gitignore. Commit requirements.txt instead.requirements.txt for production, requirements-dev.txt for development tools.pip installwhich python to verify your environment is active.venv/bin/python.Common mistakes to avoid
5 patternsForgetting to activate the environment before installing packages
pip install requests and it installs globally into /usr/local/lib/python3.x/site-packages instead of into your project. A teammate clones your repo, runs pip install -r requirements.txt, and gets ModuleNotFoundError because the requirements file is empty (since you never froze it from the right environment).(.venv) before running any pip command. If it's missing, run source .venv/bin/activate (Mac/Linux) or .venv\Scripts\activate (Windows) first. After activation, run which python to confirm it points to your project folder.Committing the .venv folder to Git
.venv are platform-specific and incompatible..gitignore file in your project root and add .venv/ to it immediately after creating the environment. Only ever commit requirements.txt, never the folder itself. If you've already committed it, remove it with git rm -r --cached .venv and add it to .gitignore.Running `pip freeze > requirements.txt` with test packages installed
requirements.txt includes development tools like pytest, black, mypy, and pre-commit that your production server doesn't need. This causes slower deployments, larger Docker images (800 MB instead of 200 MB), and potential security bloat.requirements.txt for production dependencies and requirements-dev.txt for development tools. Generate them separately. In production, only install requirements.txt. On your local machine, install both: pip install -r requirements.txt -r requirements-dev.txt. Use pip freeze | grep -E "pytest|black|mypy" > requirements-dev.txt to extract dev dependencies.Using different Python versions in environment than production
python -m venv .venv where python points to Python 3.12, but your production server runs Python 3.10. Your code works locally but crashes in production with syntax errors or missing standard library features.python3.10 -m venv .venv to specify the exact version. Consider using pyenv to manage multiple Python versions locally. In CI, test against the same Python version as production.Running activation script without `source` (Mac/Linux)
.venv/bin/activate (without source) and see no error, but which python still shows the global Python path. The prompt doesn't show (.venv). No error message appears, making the mistake hard to detect.source .venv/bin/activate (Mac/Linux) or . .venv/bin/activate (sh). The source command runs the script in the current shell, not a subshell, so its environment changes persist. Without source, the script runs in a child process that exits immediately, leaving your shell unchanged.Interview Questions on This Topic
Why would you use a virtual environment instead of just installing packages globally? Can you describe a real scenario where not using one caused a problem?
ExecStart to the venv's Python binary, solve this.Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Drawn from code that ran under real load.
That's Advanced Python. Mark it forged?
9 min read · try the examples if you haven't