Skip to content
academia.sh

Lesson 01 / 14

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.

Contents

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

"""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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close