Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 1 – Getting started: install, run and read Python

Virtual environments with venv and pip

Create a Python virtual environment with venv, install and list packages in it with python -m pip, and see why many system Pythons refuse a pip install.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create, activate and delete a virtual environment with venv
  • Install, list and remove packages with python -m pip
  • Detect from a program whether it runs in a virtual environment
  • Explain why many system Pythons refuse pip install
  • Rebuild an environment from requirements.txt instead of moving it

Before you start

What you’ll learn · Before you start

What you will learn

  • Create, activate and delete a virtual environment with venv
  • Install, list and remove packages with python -m pip
  • Detect from a program whether it runs in a virtual environment
  • Explain why many system Pythons refuse pip install
  • Rebuild an environment from requirements.txt instead of moving it

Before you start

On this page

Two projects on one computer can need two versions of the same package, one written for version 1 of a library and one for version 2. With a single Python, one pip install serves both, so an upgrade for one project can break the other, and the Python that came with your operating system may also run tools of the system itself. A virtual environment gives each project a folder of packages of its own.

Below you create an environment and look inside it, see what activating it changes, install a package into it without a download, see why many system Pythons refuse a plain pip install, and rebuild an environment instead of moving it. Every command ran with this track’s Python 3.14.8, and the page shows exactly what it printed.

A virtual environment is a folder with its own packages

A virtual environment is a folder, by convention named .venv and kept in the project’s own folder. Three things in it matter:

  • pyvenv.cfg, a small text file that names the Python the environment was made from, its base Python;
  • bin/python (Scripts\python.exe on Windows), the environment’s own python command;
  • lib/python3.14/site-packages (Lib\site-packages on Windows), where the packages you install for this project go.
A project folder holds main.py and .venv, whose python reads pyvenv.cfg, which names the base Python, and imports its own packages.Your project folderBase Python 3.14the interpreter andthe standard librarymain.pyyour own code.venv: the virtual environmentbin/python(Scripts\python.exeon Windows)site-packagesthis project'spackagespyvenv.cfgnames thebase Pythonruns withimports fromreadsnames

What is inside a virtual environment

Text description of the diagram

The diagram shows, from top to bottom, a project folder and, below it and outside it, the base Python.

  • The project folder holds your own code, main.py, and the virtual environment, a folder named .venv.
  • main.py runs with the environment's own python command, bin/python (Scripts\python.exe on Windows).
  • That python reads pyvenv.cfg and imports packages from the environment's site-packages folder (lib/python3.14/site-packages, or Lib\site-packages on Windows), where pip installs this project's packages.
  • pyvenv.cfg names the base Python 3.14 the environment was made from. The environment's python is a link to that interpreter (on Windows, a small launcher that starts it) and uses its standard library: only the packages belong to the environment.

Create one with python3 -m venv .venv in the project’s folder, or py -m venv .venv on Windows. -m venv runs the venv module of the Python you typed, so the environment is built on that Python and keeps its version. The command prints nothing when it works. On Debian it needs one more package first: Debian ships venv in a package of its own, python3-venv, and Ubuntu’s documentation lists venv among what its python3-full package adds. This program creates an environment and looks inside:

Create an environment and look inside

look_inside.py

import sys
from pathlib import Path

from session import run

# The environment's programs are in .venv/bin (.venv\Scripts on Windows).
run("python3 -m venv .venv", sys.executable, "-m", "venv", ".venv")
print("In .venv:", sorted(p.name for p in Path(".venv").iterdir()))
print("pyvenv.cfg says:")
print(Path(".venv/pyvenv.cfg").read_text(), end="")
packages = Path(".venv/lib/python3.14/site-packages")
print("site-packages holds:", sorted(p.name for p in packages.iterdir()))
print(".gitignore holds:", Path(".venv/.gitignore").read_text().splitlines()[-1])

check = "import sys; print(sys.prefix != sys.base_prefix)"
run(f".venv/bin/python -c '{check}'", ".venv/bin/python", "-c", check)
run(f"python3 -c '{check}'", sys.executable, "-c", check)

Output

$ python3 -m venv .venv
In .venv: ['.gitignore', 'bin', 'include', 'lib', 'pyvenv.cfg']
pyvenv.cfg says:
home = …/bin
include-system-site-packages = false
version = 3.14.8
executable = …/bin/python3.14
command = …/bin/python3 -m venv .venv
site-packages holds: ['pip', 'pip-26.2.1.dist-info']
.gitignore holds: *
$ .venv/bin/python -c 'import sys; print(sys.prefix != sys.base_prefix)'
True
$ python3 -c 'import sys; print(sys.prefix != sys.base_prefix)'
False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 look_inside.py

session.py

"""Runs a command as if you had typed it in a terminal, and prints what it printed."""
import os
import subprocess

# PIP_DISABLE_PIP_VERSION_CHECK: pip does not look online for a newer pip,
# so what these examples print does not depend on the network.
ENV = {**os.environ, "PIP_DISABLE_PIP_VERSION_CHECK": "1"}


def run(typed, *command):
    """Show the command as you would type it, run it, and print its output."""
    print("$", typed)
    result = subprocess.run(command, capture_output=True, text=True, env=ENV)
    print(result.stdout + result.stderr, end="")
    return result

Four things to see in that output. The standard library was not copied: site-packages holds only pip, the one package a new environment starts with, in the version this Python bundles, 26.2.1. pyvenv.cfg names the base Python’s folder in home (the page shortens that folder to …), and include-system-site-packages = false keeps the base Python’s own packages out. venv wrote a .gitignore holding *, so Git ignores the whole folder, which venv has done since Python 3.13. And sys.prefix != sys.base_prefix is True inside the environment and False outside: the venv documentation gives this comparison as the way a program can tell.

How does .venv/bin/python know that it belongs to an environment? Step through one start and one import:

Interactive · step by step How the environment's python finds its packages Step through one start of .venv/bin/python and one import, from pyvenv.cfg to the package.
How the environment's python finds its packages, step 6 of 6. Showing .venv/bin/python (sys.prefix is .venv), Base Python 3.14, .venv/pyvenv.cfg, Standard library, .venv site-packages, Base site-packages.links toone folder up.venv/bin/pythonsys.prefix is .venvBase Python 3.14the interpreter.venv/pyvenv.cfghome = …/binStandard libraryjson, pathlib and more.venv site-packagesthis project's packagesBase site-packagesnot searched

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. You start .venv/bin/python. On macOS and Linux, .venv/bin/python is usually a link to the base Python's interpreter (on Windows, a small launcher that starts it), so the base interpreter is what runs.
  2. It finds pyvenv.cfg. At start-up Python looks for pyvenv.cfg next to its program and one folder up. It finds .venv/pyvenv.cfg, so sys.prefix becomes .venv while sys.base_prefix stays the base Python.
  3. The standard library is shared. json, pathlib and the rest of the standard library still come from the base Python, which is why making an environment takes seconds and copies none of them.
  4. Packages come from .venv. The environment's own site-packages folder is searched for packages, so whatever pip installed into this environment can be imported.
  5. Base packages stay out. pyvenv.cfg says include-system-site-packages = false, so the base Python's own site-packages folder is not searched. A package installed there is invisible here.
  6. One project, one set of packages. Another project's .venv has its own site-packages and the same base Python. The two share the interpreter and the standard library, and never each other's packages.

Key idea

A virtual environment is a folder of packages with a pyvenv.cfg that points back to a base Python: the interpreter and the standard library are the base Python’s, and only the packages belong to the environment.

Checkpoint

Check: What a virtual environment is

  1. One question A program prints sys.prefix and sys.base_prefix, and the two folders differ. What does that tell you?

    Choose one answer.

    Show the answer: One question

    Answer: It runs inside a virtual environment, made from the Python in sys.base_prefix

    A python that finds a pyvenv.cfg next to it or one folder up sets sys.prefix to the environment's folder and keeps sys.base_prefix as the base Python's. The venv documentation gives sys.prefix != sys.base_prefix as the check.

Activating puts the environment’s python first on PATH

Typing .venv/bin/python every time is long. Activating the environment puts its bin folder (Scripts on Windows) first on your PATH: The environment variable that lists the folders a shell searches, in order, for a command you type. A command whose folder is not on PATH answers "command not found" until the folder is added or the full path is typed., so that python, python3 and pip mean the environment’s until you type deactivate or close the terminal. Here is a bash shell before, during and after:

What activate and deactivate change Python · activate_session.py
import subprocess
import sys

from session import run

run("python3 -m venv .venv", sys.executable, "-m", "venv", ".venv")

# The commands below run in one bash shell, one after another, as if typed.
commands = [
    "command -v python3",
    "source .venv/bin/activate",
    "command -v python3",
    'echo "$PS1"',
    "deactivate",
    "command -v python3",
]
script = "".join(f"echo '$ {line}'; {line}\n" for line in commands)
shell = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
print(shell.stdout + shell.stderr, end="")

Output

$ python3 -m venv .venv
$ command -v python3
…/bin/python3
$ source .venv/bin/activate
$ command -v python3
.venv/bin/python3
$ echo "$PS1"
(.venv) 
$ deactivate
$ command -v python3
…/bin/python3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 activate_session.py

Before activation, python3 was the base Python, this track’s (shortened to …). After source .venv/bin/activate the shell found .venv/bin/python3 first, and the prompt started with (.venv), which is how you can see that an environment is active. deactivate put both back. The command to activate depends on your shell:

Shell Activate with
bash or zsh (macOS, Linux) source .venv/bin/activate
fish source .venv/bin/activate.fish
csh or tcsh source .venv/bin/activate.csh
PowerShell (Windows) .venv\Scripts\Activate.ps1
Command Prompt (Windows) .venv\Scripts\activate.bat

If PowerShell refuses to run Activate.ps1, the venv documentation gives the command that allows such scripts for your own user: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser.

Activation is a convenience, not a requirement. The full path of the environment’s interpreter runs a program in the environment with nothing activated: .venv/bin/python main.py, or .venv\Scripts\python main.py on Windows. That is how editors and scheduled jobs use an environment.

Checkpoint

Check: Activating an environment

  1. One question What does source .venv/bin/activate change?

    Choose one answer.

    Show the answer: One question

    Answer: It puts .venv/bin first on PATH, so python, python3 and pip mean the environment's until you deactivate

    Activation is a shell convenience: it puts the environment's bin folder first on PATH and adds (.venv) to the prompt. The full path, .venv/bin/python, uses the environment without it.

pip installs into the environment whose python runs it

python -m pip runs the pip of that python, so it installs into that python’s environment. The program below first builds a small package file, a wheel: A built package, a zip file with the extension .whl, often one per platform. pip installs a wheel by unpacking its files, without running the package's own code. More in “Finding and installing third-party packages” made by make_wheel.py, so that the install needs no download: --no-index --find-links wheels tells pip to look only in that folder. Installing from PyPI: The Python Package Index, pypi.org, where most third-party Python packages are published. pip installs from it unless it is told to use another index or a folder of files. More in “Finding and installing third-party packages” works the same way, with a download:

Install into the environment, and only there

install_session.py

import sys

from make_wheel import make_wheel
from session import run

make_wheel("wheels")  # a package file to install from, so no download is needed
py = ".venv/bin/python"
run("python3 -m venv .venv", sys.executable, "-m", "venv", ".venv")
run(f"{py} -m pip --version", py, "-m", "pip", "--version")
run(
    f"{py} -m pip install --no-index --find-links wheels marksheet",
    py, "-m", "pip", "install", "--no-index", "--find-links", "wheels", "marksheet",
)
run(f"{py} -m pip list", py, "-m", "pip", "list")
where = "import marksheet; print(marksheet.__file__)"
run(f"{py} -c '{where}'", py, "-c", where)
run("python3 -c 'import marksheet'", sys.executable, "-c", "import marksheet")
run(f"{py} -m pip uninstall -y marksheet", py, "-m", "pip", "uninstall", "-y", "marksheet")

Output

$ python3 -m venv .venv
$ .venv/bin/python -m pip --version
pip 26.2.1 from .venv/lib/python3.14/site-packages/pip (python 3.14)
$ .venv/bin/python -m pip install --no-index --find-links wheels marksheet
Looking in links: wheels
Processing ./wheels/marksheet-1.0.0-py3-none-any.whl
Installing collected packages: marksheet
Successfully installed marksheet-1.0.0
$ .venv/bin/python -m pip list
Package   Version
--------- -------
marksheet 1.0.0
pip       26.2.1
$ .venv/bin/python -c 'import marksheet; print(marksheet.__file__)'
.venv/lib/python3.14/site-packages/marksheet.py
$ python3 -c 'import marksheet'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import marksheet
ModuleNotFoundError: No module named 'marksheet'
$ .venv/bin/python -m pip uninstall -y marksheet
Found existing installation: marksheet 1.0.0
Uninstalling marksheet-1.0.0:
  Successfully uninstalled marksheet-1.0.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 install_session.py

make_wheel.py

"""Builds marksheet-1.0.0-py3-none-any.whl, a tiny package, so that no download is needed.

A wheel is a zip file: the package's code, and a NAME-VERSION.dist-info folder that
says what the package is and which commands it adds.
"""
import base64
import hashlib
import zipfile
from pathlib import Path

CODE = '''def grade(mark):
    return "pass" if mark >= 35 else "fail"


def main():
    print("72 is a", grade(72))
'''


def make_wheel(folder):
    info = "marksheet-1.0.0.dist-info"
    files = {
        "marksheet.py": CODE,
        f"{info}/METADATA": "Metadata-Version: 2.4\nName: marksheet\nVersion: 1.0.0\n",
        f"{info}/WHEEL": "Wheel-Version: 1.0\nGenerator: make_wheel.py\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
        f"{info}/entry_points.txt": "[console_scripts]\nmarksheet = marksheet:main\n",
    }
    record = []
    for name, text in files.items():
        digest = base64.urlsafe_b64encode(hashlib.sha256(text.encode()).digest())
        record.append(f"{name},sha256={digest.rstrip(b'=').decode()},{len(text.encode())}")
    files[f"{info}/RECORD"] = "\n".join(record + [f"{info}/RECORD,,"]) + "\n"
    Path(folder).mkdir(exist_ok=True)
    wheel = Path(folder, "marksheet-1.0.0-py3-none-any.whl")
    with zipfile.ZipFile(wheel, "w") as archive:
        for name, text in files.items():
            archive.writestr(name, text)
    return wheel

pip --version names the folder its pip runs from, the environment’s site-packages, and the package landed there too, as .venv/lib/python3.14/site-packages/marksheet.py. So the environment’s python imports it, and the base python3 stops with ModuleNotFoundError: that isolation is what a virtual environment is for. With the environment active, plain python -m pip means the same:

Task Command
Install a package python -m pip install requests
Install a version range python -m pip install "requests>=2,<3"
Upgrade a package python -m pip install --upgrade requests
List what is installed python -m pip list
Remove a package python -m pip uninstall requests
Save the exact versions python -m pip freeze > requirements.txt
Install from that file python -m pip install -r requirements.txt

Quote a version range, as in the second row: without the quotes, the shell reads < and > as redirections. As the REPL lesson showed, python -m pip also makes sure that the pip of the right interpreter runs, where a bare pip may belong to another Python on your PATH.

Security

A package you install runs its code on your computer whenever your program uses it. Check the spelling of the name and the project’s page on pypi.org before you install: with typosquatting: Registering a package name one slip away from a popular one, such as a letter dropped, two letters swapped or a separator removed, so that a typo installs someone else's code., a name one slip away from a popular package, a typo installs someone else’s code.

Checkpoint

Check: Installing into an environment

  1. Question 1 of 2 With no environment active, you run .venv/bin/python -m pip install rich. Where is rich installed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Into the environment's site-packages, .venv/lib/python3.14/site-packages

    The python you type decides where pip installs. .venv/bin/python -m pip installs into .venv, activated or not, just as install_session.py did with marksheet.

  2. Question 2 of 2 After installing marksheet into .venv, you run python3 -c "import marksheet" with the base python3 and no environment active. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ModuleNotFoundError, because the base Python does not search .venv's site-packages

    Each python searches its own site-packages. The base python3 does not know about .venv, so the import fails with ModuleNotFoundError, exactly as install_session.py printed.

Many system Pythons refuse pip install outside an environment

Run python3 -m pip install requests with the Python of Debian 12 or later, or of Homebrew on a Mac, outside any environment, and pip stops with error: externally-managed-environment and This environment is externally managed. That Python belongs to the system’s package manager, which installs its packages itself; a pip install could replace a package that a tool of the system needs. The rule is a packaging standard, from PEP 668. Step through the check that pip 26.2.1 makes before it installs:

Interactive · step by step Does pip install, or refuse? Step through the two questions pip asks before it installs, for a system Python and for a virtual environment.
Does pip install, or refuse?, step 6 of 6. Showing pip install requests, In an environment? (prefix ≠ base_prefix), Installs the package, Marker file found? (no file here), Refuses with an error. In focus: Marker file found?.yesnoyesno filepip install requestswith the system's python3In an environment?prefix ≠ base_prefixInstalls the packageMarker file found?no file hereRefuses with an errorno file

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Outside any environment. You run python3 -m pip install requests with the Python of Debian 12 or of Homebrew, with no virtual environment active.
  2. First question. pip asks whether it runs inside a virtual environment, by comparing sys.prefix with sys.base_prefix. Here they are the same folder, so the answer is no.
  3. Second question. It looks for a file named EXTERNALLY-MANAGED in the standard library's folder. The system's package manager put one there, to say that it installs this Python's packages itself.
  4. pip refuses. pip stops with error: externally-managed-environment and shows the message from that file, which usually says to make a virtual environment. Nothing was installed or changed.
  5. Inside a virtual environment. Run the same command with .venv/bin/python and the first answer is yes. pip installs into the environment and never looks for the file, which is why an environment always works.
  6. A Python without the file. A Python with no such file, such as this track's from mise, lets pip install, but into the one base Python that every project shares. An environment is still the better place.

A program can make the same check that pip makes. This track’s Python 3.14.8 was installed by a version manager, mise, not by a system’s package manager, so every answer is False: nothing marks it as managed.

Would pip refuse to install here? Python · managed.py
import sys
import sysconfig
from pathlib import Path

in_venv = sys.prefix != sys.base_prefix
marker = Path(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")

print("Python", sys.version.split()[0])
print("Inside a virtual environment:", in_venv)
print("Marked as externally managed:", marker.exists())
print("A plain pip install would be refused:", marker.exists() and not in_venv)

Output

Python 3.14.8
Inside a virtual environment: False
Marked as externally managed: False
A plain pip install would be refused: False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 managed.py

Run the same file with the python3 of Debian 12 or Homebrew and the third line says True, and so does the fourth, unless you run it from a virtual environment, where pip is always allowed to install. When you meet the error:

  • For anything your own code imports, make a virtual environment, as in this lesson.
  • For a command-line application you want everywhere, install it with pipx, the next lesson’s tool, which gives each application an environment of its own.
  • For a library the system packages itself, install the system’s package with its package manager, as the message of Debian’s Python suggests (apt install python3-xyz).

Warning

Do not use sudo pip install, and keep away from pip’s --break-system-packages option, which, as its documentation says, allows pip to modify an externally managed installation. Both change files that the operating system’s own tools depend on.

Checkpoint

Check: System Pythons that refuse pip

  1. One question On Debian 12, python3 -m pip install requests for your own script stops with error: externally-managed-environment. What do you do?

    Choose one answer.

    Show the answer: One question

    Answer: Make a virtual environment for the script and install requests into it

    The marker says that the system's package manager owns this Python. For code you write, a virtual environment is the answer; command-line applications go to pipx, and system libraries come from the package manager.

Delete and rebuild an environment, never move it

Nothing of yours lives in .venv, so deleting it is safe: remove the folder (rm -rf .venv on macOS and Linux, Remove-Item -Recurse .venv in PowerShell, or with your file manager). Before you do, record the exact versions in a requirements file: A text file, usually requirements.txt, with one requirement per line, which pip install -r installs. pip freeze writes one that lists every installed package with its exact version. More in “Finding and installing third-party packages” with pip freeze, so that a new environment can get the same packages again.

Moving is what breaks. Every command that pip installs, such as marksheet below, is a small script whose first line, #!, holds the full path of the environment’s python, so after a move that path no longer exists. The program shows the whole cycle, from freezing to rebuilding:

Freeze, move, break and rebuild Python · move_and_rebuild.py
import os
import shutil
import subprocess
import sys
from pathlib import Path

from make_wheel import make_wheel
from session import run


def first_line(script):
    """Where the #! line of a script that pip installed points, and whether that python exists."""
    python = script.read_text().splitlines()[0].removeprefix("#!")
    print(f"  {script} starts with #! and the full path of {os.path.relpath(python)};",
          "it exists:", Path(python).exists())


make_wheel("wheels")
py = ".venv/bin/python"
find = ["--no-index", "--find-links", "wheels"]
run("python3 -m venv .venv", sys.executable, "-m", "venv", ".venv")
run(f"{py} -m pip install --no-index --find-links wheels marksheet", py, "-m", "pip", "install", *find, "marksheet")
frozen = run(f"{py} -m pip freeze", py, "-m", "pip", "freeze").stdout
Path("requirements.txt").write_text(frozen)  # what pip freeze > requirements.txt does
first_line(Path(".venv/bin/marksheet"))

print("$ mv .venv moved")
shutil.move(".venv", "moved")
first_line(Path("moved/bin/marksheet"))
print("$ moved/bin/marksheet")
try:
    subprocess.run(["moved/bin/marksheet"], check=True)
except FileNotFoundError as error:  # the python its #! line names is gone
    print("  it does not start:", error.strerror)

print("$ rm -rf moved")
shutil.rmtree("moved")
run("python3 -m venv .venv", sys.executable, "-m", "venv", ".venv")
run(f"{py} -m pip install --no-index --find-links wheels -r requirements.txt",
    py, "-m", "pip", "install", *find, "-r", "requirements.txt")
run(".venv/bin/marksheet", ".venv/bin/marksheet")

Output

$ python3 -m venv .venv
$ .venv/bin/python -m pip install --no-index --find-links wheels marksheet
Looking in links: wheels
Processing ./wheels/marksheet-1.0.0-py3-none-any.whl
Installing collected packages: marksheet
Successfully installed marksheet-1.0.0
$ .venv/bin/python -m pip freeze
marksheet==1.0.0
  .venv/bin/marksheet starts with #! and the full path of .venv/bin/python; it exists: True
$ mv .venv moved
  moved/bin/marksheet starts with #! and the full path of .venv/bin/python; it exists: False
$ moved/bin/marksheet
  it does not start: No such file or directory
$ rm -rf moved
$ python3 -m venv .venv
$ .venv/bin/python -m pip install --no-index --find-links wheels -r requirements.txt
Looking in links: wheels
Processing ./wheels/marksheet-1.0.0-py3-none-any.whl (from -r requirements.txt (line 1))
Installing collected packages: marksheet
Successfully installed marksheet-1.0.0
$ .venv/bin/marksheet
72 is a pass

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 move_and_rebuild.py

The moved command still named .venv/bin/python, which was gone, so it did not start (the page writes that full path relative to the example’s folder). The environment rebuilt from requirements.txt works again, with the same marksheet 1.0.0. The venv documentation says the same: environments are not movable or copyable, so you recreate them in the new place.

Committing or copying .venv

Putting .venv into Git, or copying it to another computer, ships a folder full of paths that only make sense on the computer that made it. Commit requirements.txt (or the project’s pyproject.toml, which the next lesson introduces) and let each person build their own environment from it.

.gitignore Generator Make a .gitignore for a Python project that keeps .venv and __pycache__ out of Git.

Checkpoint

Check: Rebuilding instead of moving

  1. One question You move a project folder, with its .venv inside, to another place on the disk. What stops working?

    Choose one answer.

    Show the answer: One question

    Answer: The commands pip installed, because their first line names the old path of the environment's python

    Installed commands hold the full path of the environment's python on their #! line, so a moved environment's commands no longer start. Delete it and rebuild it from requirements.txt in the new place.

Practise: check an environment

Two short functions use the ideas of this lesson: one tells whether a program runs in a virtual environment, the other reads the versions that pip freeze recorded. Both steps come with starter code, a first hint and sample tests you can run here in the page, so do them in either order:

Code lab · 2 steps

Check an environment

Tell whether a Python runs in a virtual environment, then read the exact versions that pip freeze recorded.

  1. Tell whether a Python runs in a virtual environment To do
  2. Read the versions that pip freeze recorded To do

Exercise · Easy · Python

Tell whether a Python runs in a virtual environment

Write in_virtualenv(sys_module). It is given an object with the attributes prefix and base_prefix, such as the sys module itself, and returns True when the Python it describes runs inside a virtual environment, False when it is a base Python.

  • Inside an environment, prefix is the environment's folder and base_prefix the base Python's, so they differ.
  • In a base Python they are the same folder.
  • The tests pass fake objects as well as the real sys, so read the attributes of sys_module, not of sys.

Starter code · venvcheck.py

def in_virtualenv(sys_module):
    """Return True if the Python that sys_module describes runs in a virtual environment."""
    return None
The sample tests · test_venvcheck.py
import sys
from types import SimpleNamespace

from venvcheck import in_virtualenv


def test_inside_an_environment():
    """a prefix that differs from base_prefix means a virtual environment"""
    fake = SimpleNamespace(prefix="/home/asha/marks/.venv", base_prefix="/usr")
    assert in_virtualenv(fake) is True


def test_base_python():
    """the same prefix and base_prefix means the base Python itself"""
    fake = SimpleNamespace(prefix="/usr", base_prefix="/usr")
    assert in_virtualenv(fake) is False


def test_windows_paths():
    """works the same with Windows paths"""
    fake = SimpleNamespace(prefix="C:\\marks\\.venv", base_prefix="C:\\Python314")
    assert in_virtualenv(fake) is True


def test_this_python():
    """agrees with the sys module of the Python that runs the tests"""
    assert in_virtualenv(sys) == (sys.prefix != sys.base_prefix)
A hint

The whole check is one comparison of sys_module.prefix with sys_module.base_prefix. The comparison already gives True or False, so return it directly.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Easy · Python

Read the versions that pip freeze recorded

Write pinned_versions(text). text is what pip freeze printed (the contents of a requirements.txt), and the function returns a dict that maps each package name to its exact version.

  • A line such as marksheet==1.0.0 gives {"marksheet": "1.0.0"}.
  • Blank lines and comments (lines that start with #) are skipped, and so are spaces around a line.
  • A package installed straight from a file appears as marksheet @ file:///…/marksheet-1.0.0-py3-none-any.whl#sha256=…, with no ==: skip that line too, because it records a file, not a version.
  • Empty text gives an empty dict.

Starter code · frozen.py

def pinned_versions(text):
    """Return {name: version} for every name==version line of pip freeze's output."""
    versions = {}
    return versions
The sample tests · test_frozen.py
from frozen import pinned_versions


def test_two_pins():
    """reads every name==version line"""
    text = "marksheet==1.0.0\ntablekit==2.3.1\n"
    assert pinned_versions(text) == {"marksheet": "1.0.0", "tablekit": "2.3.1"}


def test_blank_lines_and_comments():
    """skips blank lines and comments"""
    text = "# written by pip freeze\n\nmarksheet==1.0.0\n"
    assert pinned_versions(text) == {"marksheet": "1.0.0"}


def test_spaces_around_a_line():
    """ignores spaces around a line"""
    assert pinned_versions("  marksheet==1.0.0  \n") == {"marksheet": "1.0.0"}


def test_a_file_instead_of_a_version():
    """skips a line that records a file instead of a version"""
    text = "marksheet @ file:///home/asha/wheels/marksheet-1.0.0-py3-none-any.whl\ntablekit==2.3.1\n"
    assert pinned_versions(text) == {"tablekit": "2.3.1"}


def test_empty_text():
    """gives an empty dict for empty text"""
    assert pinned_versions("") == {}
A hint

Loop over text.splitlines(), strip() each line, and continue when it is empty, starts with # or has no ==. line.split("==", 1) then gives the name and the version.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

When both steps pass, try the questions below. They pick the idea of this lesson you are least sure of, and the feedback on a wrong choice names what it overlooks:

Practice that adapts to you

Practise this lesson

11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 What does python3 -m venv .venv put into the new folder?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: A pyvenv.cfg, a python command that starts the base interpreter, and a site-packages folder with pip in it

    A new environment holds pyvenv.cfg, a python command (a link to the base interpreter on macOS and Linux, a launcher on Windows), the activation scripts and a site-packages folder that starts with only pip.

  2. Question 2 of 11 You delete .venv/pyvenv.cfg and run .venv/bin/python -c "import sys; print(sys.prefix != sys.base_prefix)". What does it print?

    Choose one answer.

    A hint

    Which file tells a starting python that it belongs to an environment?

    Show the answer: Question 2 of 11

    Answer: False

    A python becomes an environment's python by finding pyvenv.cfg next to its program or one folder up. Without the file, .venv/bin/python starts as the plain base Python, and sys.prefix equals sys.base_prefix.

  3. Question 3 of 11 A scheduled job runs /home/asha/report/.venv/bin/python report.py and never activates anything. Does report.py get the environment's packages?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: Yes, because the full path names the environment's python

    Calling the environment's interpreter by its full path is the dependable way for editors and jobs to use an environment: no activation, no dependence on PATH.

  4. Question 4 of 11 What does deactivate do?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: It puts PATH and the prompt back, so python means the base Python again

    deactivate reverses activation in the current shell: PATH and the prompt return to what they were, and the environment itself is unchanged.

  5. Question 5 of 11 Why is python -m pip install … safer than a bare pip install …?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: It runs the pip of that python, so the package goes where that python looks for it

    python -m pip ties pip to one interpreter. With several Pythons installed, a bare pip can install into another one than the python that will run your program.

  6. Question 6 of 11 With .venv active, you want to see what is installed in it, then take marksheet out again. Which two commands do that?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: python -m pip list, then python -m pip uninstall marksheet

    python -m pip list shows the packages of the environment whose python runs it, and python -m pip uninstall takes one out again. install_session.py ran both, with -y on uninstall so that it did not wait for a yes.

  7. Question 7 of 11 Why does python -m pip install requests>=2,<3 go wrong in a shell, while python -m pip install "requests>=2,<3" works?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: Without quotes the shell reads > and < as redirections, so pip never sees the whole requirement

    The shell reads > and < as redirections before any program runs. Quoting the whole requirement passes it to pip as one argument.

  8. Question 8 of 11 How does pip know that a Python is externally managed?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: A file named EXTERNALLY-MANAGED in that Python's standard library folder

    The packaging standard puts a file named EXTERNALLY-MANAGED in the standard library folder. Outside an environment pip finds it and refuses, showing the message the file contains.

  9. Question 9 of 11 On Debian 12 you make an environment with the system's python3 -m venv .venv, then run .venv/bin/python -m pip install requests. What happens?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: pip installs requests into .venv, because inside an environment it does not look for the marker file

    Externally managed means "do not change this Python with pip". An environment built from it has its own site-packages, and pip installs there without complaint.

  10. Question 10 of 11 Put the steps in order to replace a project's environment with a fresh one that has the same packages.

    Give each item its position, from 1 (first).

    Show the answer: Question 10 of 11

    Answer:

    1. .venv/bin/python -m pip freeze > requirements.txt
    2. rm -rf .venv
    3. python3 -m venv .venv
    4. .venv/bin/python -m pip install -r requirements.txt

    Record the exact versions first, with the environment's own python, while the old environment still exists; then delete it, make a new one and install from the file. Freezing after the delete would record nothing, and freezing with the base python3 would record the base Python's packages instead.

  11. Question 11 of 11 Which file do you commit so that a teammate can build the same environment?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: requirements.txt, with the exact versions that pip freeze wrote

    Commit the list of packages, not the folder: requirements.txt (or pyproject.toml) is short, has no paths, and rebuilds the environment anywhere.

Interview questions

Warm-up (fresher): what is a virtual environment, and why use one for each project?

A virtual environment is a folder, usually .venv inside the project, with its own python command and its own site-packages folder, made by python3 -m venv .venv. Its pyvenv.cfg points to the base Python, whose interpreter and standard library it uses, so only the packages are its own. One environment per project means each project gets exactly the versions it needs: one can use version 1 of a library while another uses version 2, an upgrade in one cannot break the other, and the system’s own Python stays untouched. The environment can be deleted and rebuilt from requirements.txt at any time, which is why it is never committed: you commit the list of packages, not the folder.

Two harder questions start from this lesson: a server that refuses pip install, and a scheduled job that cannot find a package your terminal can. Work out your first step before you open each one:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    On a new Debian 12 server, python3 -m pip install fails with error: externally-managed-environment. A teammate suggests sudo pip install --break-system-packages. What do you do instead, for a web app the server runs and for a command-line tool you use there, and why?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    pip install requests worked in your terminal, and your script runs there. The same script, started by a nightly scheduled job, stops with ModuleNotFoundError: No module named 'requests'. Explain why, and fix it so that it cannot happen again.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A virtual environment is a folder (.venv) with its own python and site-packages, built on a base Python whose interpreter and standard library it shares; create it with python3 -m venv .venv, or py -m venv .venv on Windows.
  • Activating puts the environment’s bin folder first on PATH; .venv/bin/python runs a program in the environment without activating anything.
  • python -m pip installs into the environment of the python that runs it, and only there, which is why the base Python cannot import the package.
  • error: externally-managed-environment means that the Python belongs to the system: use a virtual environment or pipx, never sudo pip or --break-system-packages.
  • Record exact versions with pip freeze > requirements.txt, and rebuild an environment from that file instead of moving or copying it, because installed scripts hold its full path.
  • sys.prefix != sys.base_prefix tells a program that it runs inside a virtual environment.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.