---
title: 'Virtual Environments'
source: 'https://academia.sh/en/courses/python-projects/virtual-environments'
course: 'Python Projects: Packaging and Testing'
language: en
updated: '2026-08-17T18:10:31+00:00'
license: 'CC BY-SA 4.0'
---

# Virtual Environments

The same resolution gives five distinct outcomes across six installs on a shared site, and the project sees the set it resolved itself on only two of the six orders; on an isolated site the number drops to one, and all six orders install the same set.

The previous course counted steps instead of time across ten lessons, and left one sentence
at its close: measuring is a discipline; so is packaging and testing. That course's question
was **what** the measurement counted — whether setup counted toward the total, where the
boundary was drawn, what name the unit was written under.

This course's question steps back one level. There is a more basic question that has to be
answered before a number's boundary can even be written: **does the same input give the same
result every time?** If a measurement gives two separate values across two runs, no matter
how well you write its boundary, it cannot be compared. This course asks that question in
three separate places in a project: its dependencies, its tests, and the package it publishes.
The first place is the one deciding which packages the code will run into.

## The Course's Measure: Distinct Outcome

A project's number is not whether it runs. A project that runs might not run on a second
machine, and this second outcome does not invalidate the first — both are true at once, just
like the previous course's six statements. What can be counted is this: **how many distinct
outcomes did the same input give?**

The course's measure is named **distinct outcome**. An installation's outcome is the set of
versions installed; if two installations give the same set, that counts as one outcome, if
they give a different set, that counts as two. A number not changing across two runs is
called **reproducibility**, and every measurement in this course searches for it.

The rig is made of three packages: `metrics`, `report`, and `common`. Each has published
versions in a registry, and the project asks for them with a **dependency declaration**: the
smallest and largest accepted version, upper bound excluded. A declaration does not name a
version, it names a range — this distinction is the next lesson's subject.

No lesson in this course writes a tool's name. The virtual environment tool,
**package manager**, test framework, and build system are referred to only by **class name**;
their behavior is **modeled** in the lesson, and that it is modeled is stated. The standard
library's `venv` and `sysconfig` modules belong to the language itself and can be named
directly. In the measurement, **no real installation is performed, no real package is
downloaded**; installation is modeled as an operation that writes to a dict.

## What Isolation Separates

A **virtual environment** is the isolation boundary that makes the location the interpreter
searches for packages specific to a project. This lesson calls the directory installed
packages land in the **site**; the interpreter looks there during import. In the standard
library, the `sysconfig` module gives this layout by scheme name, and the `venv` module
produces a new layout. The real path is not written, because the path is machine-dependent
and not reproducible.

What isolation provides is not a privacy boundary. The only thing it provides is this: **the
same-named package can exist at two separate versions in two projects.** With a single site,
this is impossible, because one name corresponds to one version in one place.

A direct consequence follows from this. When a single site is shared, the versions a project
sees depend not only on its own declaration but also on **the other projects sharing the
site, and the install order**. This is what is going to be measured.

## Where the Boundary Is Drawn

What a virtual environment is not shapes the measurement as much as what it is, because
everything left outside the boundary keeps producing distinct outcomes.

A virtual environment **does not duplicate the interpreter**; it attaches to an existing
interpreter and gives it a separate site. Setting up the environment amounts to nothing more
than making the layout that interpreter looks at during import specific to the project.
Activation is not a separate mechanism either, it is the same thing's shell-side
counterpart: which interpreter gets invoked and which site it sees is decided there. In the
standard library, this layout's names sit in `sysconfig` schemas; producing a new one is
`venv`'s job.

One consequence connects directly to this topic's last lesson: isolation separates
**packages**, not **the interpreter**. An environment is meaningful only with the interpreter
it is attached to; when the interpreter changes, the environment has to be rebuilt, since part
of the site's packages may not have been built for that interpreter.

A second thing left outside is the operating-system level. Shared system libraries, the
compiler, environment variables, and the file system sit outside a virtual environment's
boundary; if a package depends on one of these, two distinct outcomes are possible on two
machines all over again. The tools drawing this wider boundary belong to a different class and
are not this course's subject. The boundary measured here stops at one level: **the same
machine, the same interpreter, two projects' package sets.**

## An Environment's Lifecycle

The install flow is four steps: the environment is set up, the declaration is resolved, the
chosen versions are installed to the site, and the project runs through that site. After the
fourth step, the environment is now an **artifact** — a produced result, not a written source.

This distinction's practical consequence is clear: the environment is not put under version
control. The reason is not its size, it is that **its source lives somewhere else**. What will
reproduce the environment is the declaration; the environment itself is one run of that
declaration. If two runs give the same set, there is no payoff to keeping the environment.

A test follows from this. If rebuilding the environment from the same declaration loses
something, that thing was never written into the declaration — a hand-installed package, a
hand-corrected version, a residue accumulated inside the environment. Deletion, for this
reason, is not a maintenance chore, it is **a measurement**: nothing says more directly
whether a declaration is complete.

The measurement question's second half follows from this too. When the environment is deleted
and rebuilt, does the **same set** come back? On an isolated site, with neighbors and order
eliminated, one variable remains: the resolution itself. This lesson's measurement holds that
variable fixed and counts only isolation's share.

## On a Shared Site, the Last Installer Wins

Three projects use the same site. **A** is our project: it resolves its declaration and picks
the newest eligible versions. **B** and **C** are neighbor projects; both arrive with their own
locks, meaning the versions they will install are already written down and they do not
resolve. The three projects' package sets partly overlap.

The model's installation behavior is a single line: installing a package overwrites the
version on the site. **The last installer wins.** This is the unavoidable behavior of a
single-site layout; since one name can be bound to one version, the last project installing it
turns the binding in its own favor.

A's resolution is identical across all six orders, and the measurement prints this
separately. What changes is not the set A resolved, it is the set A **sees at run time**.

The measurement's assumptions:

- **ED1** — The oracle is the rig itself: we wrote the registry's versions and the three
  projects' requests ourselves, so the correct result of every order is known without
  measuring.
- **ED2** — The registry and the declaration are taken from the shared definition unchanged;
  the lesson does not modify either.
- **ED3** — A resolves its declaration with the **newest eligible version** strategy. The
  strategy is fixed in this lesson; what happens when it changes is the next two lessons'
  subject.
- **ED4** — B and C do not resolve; the versions they will install are written in their locks
  and are part of the rig.
- **ED5** — Installation overwrites the version on the site. There is no version comparison or
  "already installed" check; the model's only rule is that the last installer wins.
- **ED6** — In the shared regime there is a single site, and all three install to it. In the
  isolated regime, every project has its own site.
- **ED7** — What is measured is the `metrics`, `common`, and `report` versions standing on A's
  site at run time; what the other projects see is not counted.
- **ED8** — The six orders are all install orders of the three projects; none is privileged. On
  a real machine, what decides the order is the installs' schedule, and the project does not
  choose it.
- **ED9** — Installation is an operation that writes to a dict. **No real installation is
  performed, no real package is downloaded, no file is left in a registry.**
- **ED10** — Duration is not measured. What is counted is the **distinct outcome** and the
  number of orders producing each one.
- **ED11** — The measurement is a single run and deterministic; there is no randomness in the
  rig.

## Measurement

```python
"""Isolation: same resolution, separate site; how many distinct outcomes does install order give."""
from itertools import permutations

REGISTRY = {
    "metrics": [(1, 0), (1, 1), (1, 2), (2, 0)],
    "report": [(0, 9), (1, 0), (1, 1)],
    "common": [(3, 0), (3, 1), (3, 2), (4, 0)],
}
DECLARATION = {"metrics": ((1, 0), (2, 0)), "report": ((1, 0), (2, 0)),
               "common": ((3, 0), (4, 0))}

# Neighbor projects come with their own locks; they use the same registry at separate versions.
NEIGHBORS = {"B": {"common": (3, 0), "report": (1, 0)},
             "C": {"metrics": (1, 1), "common": (3, 1)}}
PACKAGES = ("metrics", "common", "report")


def eligible(package):
    lo, hi = DECLARATION[package]
    return [s for s in REGISTRY[package] if lo <= s < hi]


def resolve(strategy="newest"):
    chosen = {}
    for package in sorted(DECLARATION):
        candidates = eligible(package)
        chosen[package] = candidates[-1] if strategy == "newest" else candidates[0]
    return chosen


def write(site, chosen):
    """Installation overwrites the version on the site: the last installer wins."""
    site.update(chosen)
    return site


def choice_of(name):
    return resolve() if name == "A" else NEIGHBORS[name]


def shared(order):
    site = {}
    for name in order:
        write(site, choice_of(name))
    return tuple(site[p] for p in PACKAGES)


def isolated(order):
    sites = {"A": {}, "B": {}, "C": {}}
    for name in order:
        write(sites[name], choice_of(name))
    return tuple(sites["A"][p] for p in PACKAGES)


def format_versions(view):
    return " / ".join(".".join(map(str, s)) for s in view)


ORDERS = list(permutations(("A", "B", "C")))
EXPECTED = tuple(resolve()[p] for p in PACKAGES)

print(f"{'install order':<16s} {'A on shared site':>18s} {'A on isolated site':>19s}")
p_view, y_view = [], []
for order in ORDERS:
    a, b = shared(order), isolated(order)
    p_view.append(a)
    y_view.append(b)
    print(f"{' '.join(order):<16s} {format_versions(a):>18s} {format_versions(b):>19s}")

print(f"\nA's resolution is {format_versions(EXPECTED)} across all six orders")
print(f"six install orders, {len(set(p_view))} distinct outcomes on the shared site, "
      f"{len(set(y_view))} on the isolated site")
print(f"orders seeing its own resolved set: shared {p_view.count(EXPECTED)}/{len(ORDERS)}, "
      f"isolated {y_view.count(EXPECTED)}/{len(ORDERS)}")
```

```
install order      A on shared site  A on isolated site
A B C               1.1 / 3.1 / 1.0     1.2 / 3.2 / 1.1
A C B               1.1 / 3.0 / 1.0     1.2 / 3.2 / 1.1
B A C               1.1 / 3.1 / 1.1     1.2 / 3.2 / 1.1
B C A               1.2 / 3.2 / 1.1     1.2 / 3.2 / 1.1
C A B               1.2 / 3.0 / 1.0     1.2 / 3.2 / 1.1
C B A               1.2 / 3.2 / 1.1     1.2 / 3.2 / 1.1

A's resolution is 1.2 / 3.2 / 1.1 across all six orders
six install orders, 5 distinct outcomes on the shared site, 1 on the isolated site
orders seeing its own resolved set: shared 2/6, isolated 6/6
```

## Six Orders, Five Outcomes

The left column gives five distinct sets across six rows. Only two orders produce the same
result (`B C A` and `C B A`), and what they share is that A installed **last** in both. Each
of the remaining four orders gives its own set.

The first row gives the extreme case. In order `A B C`, A installs first and writes all three
of its packages with its own chosen versions; then B writes over `common` and `report`, then C
writes over `metrics` and `common`. In the end, **all three** of the versions A sees come from
other projects — none of its own decisions survive. In order `B A C`, only `report` survives,
because C never installs that package. In order `C A B`, the only surviving package is
`metrics`; `common` and `report` come last from B.

None of these five outcomes violates the declaration. A's declaration accepts all three
versions for `common`, all three for `metrics`, and both for `report`. So no matter what order
A installs in, it meets a version it accepts, and no warning is ever raised. This is where the
defect's **silence** comes from: since the declaration is a range, every deviation inside that
range is legitimate.

Only one of the five sets is A's own choice. A never chose the remaining four, never wanted
them, and never wrote them anywhere; and yet its code ran on top of them. This shows why
distinct outcome is a separate measure from "worked" or "did not work": the project can run on
all six of the six orders, none of them may raise an error, and five separate installations can
still stand behind that.

The right column shows the same six orders under the isolated regime and gives a single set:
**1 distinct outcome**. Across all six orders, A sees the set it resolved itself.

The bottom row writes the difference in its sharpest form. On the shared site, A sees the
result of its own resolution on **two of six** orders; on the isolated site, on **all six**.
The source of a three-quarters deviation is not in A's own decisions at all, it is entirely
outside them.

## What Isolation Does Not Make Uniform

Isolation solves one thing and does not solve another. This split is the frame for the rest of
the course.

**What it solves:** neighbor projects' decisions and install order no longer change the
outcome. Five distinct outcomes dropped to one, and what dropped them was not a better
resolution, it was **the boundary itself**. A's resolution was the same at the start of the
measurement and at the end; what changed was whether the resolution's result was preserved.

**What it does not solve:** A's resolution **itself**. Even on an isolated site, the set
installed depends on which rule the resolution was made with. The measurement held this rule
fixed — the newest eligible version — and that is why the right column gave a single set. Had
the rule changed, the right column would have changed too, because the declaration states not
a version but a range.

A second boundary sits even further out: the same declaration can give a separate set again
when the registry changes. Today's newest version may not be tomorrow's, and the declaration
accepts the new one too. Isolation does nothing against either of these; the boundary ends at
the site's wall.

A virtual environment is therefore **not the whole of reproducibility, it is its first
condition.** Drawing the boundary is not the same thing as deciding what happens inside it.

## Summary

- The course's measure is **distinct outcome**: a project's number is not whether it runs, it
  is how many distinct outcomes the same input gives.
- A **virtual environment** is an isolation boundary; the only thing it provides is that the
  same-named package can exist at two separate versions in two projects.
- When a single site is shared, install order decides the outcome: six orders give **5
  distinct outcomes**, and the project sees the set it resolved itself in only **2/6** orders.
- On an isolated site, the same six orders give **1 distinct outcome**, and the project sees
  its set in **6/6** orders; A's resolution is the same in both regimes — what is preserved is
  the resolution's **result**.
- None of the five outcomes violates the declaration; since the declaration is a range, a
  deviation within it is silent and raises no warning.
- Isolation eliminates the neighbor and the order, but it does not eliminate the resolution's
  own rule or the registry changing over time.

## Next Step

The single outcome on the isolated site was reached by holding one rule fixed in the
measurement: the newest eligible version. That rule was a choice, and other choices are just
as valid under the declaration. The next lesson looks inside the boundary and asks the same
question once more: **on its own, how many distinct version sets does the same declaration
specify?** The measurement puts two rules side by side, writes the resulting number to a file,
and tests whether that file eliminates the resolution altogether.
