Lesson 04 / 14
Version Management
The same declaration gives 2 distinct version sets across three rig interpreter versions; of nine installs made from lock files produced on one interpreter, 6 are accepted and 3 rejected, and the same lock gives 1 distinct outcome on a fixed interpreter but produces one rejection across three interpreters.
Contents
The previous three lessons measured how many distinct version sets the same declaration gives across three separate axes: the isolation boundary, the resolution strategy, and the resolver itself. All three carried one unwritten assumption — a single interpreter. While package versions were being compared, it was accepted that all of them could be installed on the same interpreter.
This lesson puts that assumption into the measurement. Its question has two parts: how many distinct outcomes does the same project give across separate interpreter versions, and what does a lock file produced on one interpreter do on another? The second part’s answer will pin down exactly what kind of guarantee a lock is.
The Interpreter Is a Dependency Too
A project counts its packages when writing its own declaration; it often skips counting the interpreter. But the interpreter is something installed too, it has a version, and when that version changes, the behavior the project meets can change.
This lesson writes no real version number. Interpreters are referred to with fictional
labels: Y1, Y2, Y3. The labels’ order is a time order — Y1 the oldest, Y3 the
newest — and implies nothing beyond that. Package versions are fictional too, as in the
previous three lessons.
The measurement sets up a simple table: every package version declares which interpreter versions it can be installed on. This is not fiction, it is a real constraint; a package version is written against a particular language level, and on an interpreter not meeting that level, it either cannot be installed or does not run. In the rig, this relationship is written as a set and is modeled; no product name appears.
A direct consequence follows: the interpreter narrows the candidate pool. The declaration has not changed, the registry has not changed, the strategy has not changed; but the list of versions selectable from the same range shortens depending on the interpreter. Resolution applies the same rule to a separate pool and returns a separate set.
The Environment’s Dependence on the Interpreter
The first lesson said a virtual environment does not duplicate the interpreter, it attaches to it. This measurement shows the cost of that sentence.
An environment is meaningful only with the interpreter it was built on. When the interpreter version changes, the environment does not carry over: part of the site’s packages may not have been built for the new interpreter, and the import path is also named after the interpreter version. The switch is therefore not an upgrade, it is setting up a new environment — and what gets installed into the new one is resolved afresh.
Does the same hold for a lock file? A lock writes exact versions and does not resolve; so it ought to install the same set on the new interpreter too. This is exactly the claim the measurement tests, and the answer cannot be given in one word.
The measurement’s assumptions:
- ED37 — The registry, declaration, and range rule are taken from the shared definition unchanged; the lesson does not change either’s behavior. The only addition is the compatibility table.
- ED38 — Three interpreter versions are fictional and referred to by the labels
Y1,Y2,Y3. No real version number is written. - ED39 — The compatibility table is part of the rig and part of the oracle: we wrote which interpreters each package version can be installed on, so every row’s correct result is known without measuring.
- ED40 — Compatibility is a binary decision: a version either can or cannot be installed on an interpreter. Partial working or install-with-warning is not modeled.
- ED41 — Resolution narrows the candidate pool first by the declaration, then by the compatibility table. Order does not change the result; both filters take an intersection.
- ED42 — Strategy is fixed in this lesson: newest eligible version. The strategy axis was measured in the previous two lessons and is not repeated here.
- ED43 — Packages do not require each other in this lesson; the requirement axis was measured in the previous lesson. The one new axis measured is the interpreter.
- ED44 — A lock file writes exact versions and does not resolve. Installing with a lock requires every written version to be installable on the target interpreter.
- ED45 — Install rejection is not an error, it is a measurement result, and it is
counted as
rejectin the table. A rejected install produces no distinct outcome. - ED46 — Every interpreter’s lock is produced from its own resolution; nine installs are the three locks applied to the three interpreters.
- ED47 — Switching between interpreter versions is rebuilding the environment; the old site does not carry over, in the model either.
- ED48 — No real installation is performed, no real package is downloaded, no real interpreter is invoked. An interpreter is a label, an install is a dict.
- ED49 — Duration is not measured. What is counted is the distinct version set, the accept, and the reject count.
- ED50 — The measurement is a single run, deterministic, and there is no randomness in the rig.
Measurement
"""Interpreter versions: same project, same declaration, how many distinct outcomes.""" 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))} PACKAGES = ("metrics", "common", "report") # Fictional interpreter versions; not real version numbers. INTERPRETERS = ("Y1", "Y2", "Y3") # Package version -> interpreters it supports COMPATIBILITY = { ("metrics", (1, 0)): {"Y1", "Y2"}, ("metrics", (1, 1)): {"Y1", "Y2"}, ("metrics", (1, 2)): {"Y2", "Y3"}, ("metrics", (2, 0)): {"Y3"}, ("common", (3, 0)): {"Y1", "Y2"}, ("common", (3, 1)): {"Y1", "Y2", "Y3"}, ("common", (3, 2)): {"Y2", "Y3"}, ("common", (4, 0)): {"Y3"}, ("report", (0, 9)): {"Y1"}, ("report", (1, 0)): {"Y1", "Y2"}, ("report", (1, 1)): {"Y2", "Y3"}, } def eligible(package, interpreter): lo, hi = DECLARATION[package] return [s for s in REGISTRY[package] if lo <= s < hi and interpreter in COMPATIBILITY[(package, s)]] def resolve(interpreter, strategy="newest"): chosen = {} for package in sorted(DECLARATION): candidates = eligible(package, interpreter) if not candidates: return None chosen[package] = candidates[-1] if strategy == "newest" else candidates[0] return chosen def lock(chosen): return dict(chosen) def install(lock_file, interpreter): """A lock writes every version exactly; if the interpreter does not support it, install is rejected.""" if all(interpreter in COMPATIBILITY[(p, s)] for p, s in lock_file.items()): return dict(lock_file) return None def format_selection(chosen): if chosen is None: return f"{'rejected':>23s}" return " ".join(f"{'.'.join(map(str, chosen[p])):>7s}" for p in PACKAGES) def format_short(chosen): if chosen is None: return f"{'reject':>12s}" return f"{'/'.join('.'.join(map(str, chosen[p])) for p in PACKAGES):>12s}" def key(chosen): return tuple(sorted(chosen.items())) print(f"{'resolution by declaration':<26s} {'metrics':>7s} {'common':>7s} {'report':>7s}") resolutions = {} for interpreter in INTERPRETERS: resolutions[interpreter] = resolve(interpreter) print(f"{'interpreter ' + interpreter:<26s} " + format_selection(resolutions[interpreter])) print(f"same declaration, three interpreters: {len({key(s) for s in resolutions.values()})} " f"distinct version sets") print() print(f"{'interpreter the lock was produced on':<30s} " + " ".join(f"{'install ' + interpreter:>12s}" for interpreter in INTERPRETERS)) accepted, sets = 0, [] for source in INTERPRETERS: current_lock = lock(resolutions[source]) cells = [] for target in INTERPRETERS: s = install(current_lock, target) accepted += s is not None if s is not None: sets.append(key(s)) cells.append(format_short(s)) print(f"{'lock produced on ' + source:<30s} " + " ".join(cells)) print(f"nine installs: {accepted} accepted, {9 - accepted} rejected; among the accepted " f"{len(set(sets))} distinct version sets") print() lock_y2 = lock(resolutions["Y2"]) fixed = [install(lock_y2, "Y2") for _ in range(3)] roaming = [install(lock_y2, interpreter) for interpreter in INTERPRETERS] for name, installs in (("same lock, three installs on Y2", fixed), ("same lock, three interpreters", roaming)): succeeded = [s for s in installs if s is not None] print(f"{name:<30s} {len({key(s) for s in succeeded})} distinct outcome, " f"{len(installs) - len(succeeded)} rejected")
resolution by declaration metrics common report interpreter Y1 1.1 3.1 1.0 interpreter Y2 1.2 3.2 1.1 interpreter Y3 1.2 3.2 1.1 same declaration, three interpreters: 2 distinct version sets interpreter the lock was produced on install Y1 install Y2 install Y3 lock produced on Y1 1.1/3.1/1.0 1.1/3.1/1.0 reject lock produced on Y2 reject 1.2/3.2/1.1 1.2/3.2/1.1 lock produced on Y3 reject 1.2/3.2/1.1 1.2/3.2/1.1 nine installs: 6 accepted, 3 rejected; among the accepted 2 distinct version sets same lock, three installs on Y2 1 distinct outcome, 0 rejected same lock, three interpreters 1 distinct outcome, 1 rejected
Two Sets, Three Interpreters
The top table resolves the same declaration on three interpreters and gives 2 distinct version sets.
The Y1 row gives 1.1 / 3.1 / 1.0. These are not the newest of all three; metrics 1.2,
common 3.2, and report 1.1 cannot be installed on this interpreter and drop out of the
pool. The strategy is still “newest eligible version”; what changes is what counts as
eligible. Y2 and Y3, in turn, give the same set: 1.2 / 3.2 / 1.1.
Two observations follow from this. First, the interpreter axis on its own produces a
distinct outcome: two installs were reached without touching the declaration, the strategy, or
the registry. Second, the number is not the same as the interpreter count. Three interpreters
gave two sets because Y2 and Y3 allow the same candidates. The distinct-outcome count is a
function not of how many environments there are, but of the compatibility table.
The Y1 row has one more side effect. A project running on that interpreter never sees the
packages carrying the defect fixes made in later versions, even if its declaration would
accept them. Keeping an interpreter in place is also keeping package versions in place.
A Lock Is Produced for an Interpreter
The middle table gives nine installs: three locks, three target interpreters. 6 accepted, 3 rejected.
Every row has its own pattern. The lock produced on Y1 installs on Y1 and Y2, and is
rejected on Y3 — metrics 1.1 and report 1.0 cannot be installed on that interpreter.
The locks produced on Y2 and Y3 write the same set, and both are rejected on Y1.
Reading what the rejection is has to be done correctly. This is not an upgrade error; it is the lock’s correct behavior. A lock writes exact versions and does not negotiate; if the version it writes cannot be installed on the target environment, its only option is to stop. The alternative would be silently bending it and installing a different version — at which point it would stop being a lock.
The six accepted installs give 2 distinct version sets. This number is the same as the top table’s, and that is not a coincidence: the locks were also produced from those same resolutions. A lock does not create a new set, it carries an existing one.
The Axis a Lock Guarantees
The bottom two rows are the lesson’s conclusion, and write exactly what kind of guarantee a lock is.
The same lock, installed three times on a fixed interpreter, gives 1 distinct outcome, 0 rejected. Installed on three separate interpreters, it gives 1 distinct outcome again, but 1 rejected. The lock never produced a second set anywhere — it kept its word. What it could not keep was something else: it could not keep its word in every environment.
The lock’s limit follows from this: a lock guarantees reproducibility within one interpreter; it does not guarantee it across the interpreter axis. On that axis, a lock is not a guarantee, it is a constraint — and being one is not a defect, it is its function. A lock’s inability to travel is a declaration that it should not travel.
The practical counterpart is a direct rule: a lock file should also write the interpreter version it was produced on. Without that, two documents are left on hand — one says which versions to install, the other does not say where it is valid — and the rejection only surfaces at install time. The declaration carrying a range for the interpreter too follows the same reasoning: if the project does not say which interpreters it runs on, the compatibility table can only be discovered by attempting the install.
A third consequence sits outside the numbers. An interpreter switch looks like maintenance work, but the measurement shows it is a resolution job: on switching, the lock is regenerated, the new set can come out separate from the old one, and the project has not been tested against that new set. The switch’s cost is not setting up the interpreter, it is validating the new set.
Interpreters Standing Side by Side
The measurement’s three rows assume three interpreters can exist on one machine at once, and this assumption matches reality. Tools that keep multiple interpreter versions side by side form a class — a version manager — and their job is a choice: which interpreter gets invoked in which directory. Product names are not written here either.
Where the choice is made matters. A virtual environment attaches to an interpreter the moment it is set up; from that point on, which interpreter a project uses is not a configuration setting, it is the environment’s identity. A version manager chooses which interpreter is used when a new environment is set up — it does not hand an existing environment off to another interpreter.
The switch flow is therefore five steps, and none can be skipped: install the new interpreter, set up a new environment on it, resolve the declaration again, produce a new lock, test the project against the new set. Only after this is the old environment discarded. The measurement’s middle table shows exactly why the third and fourth steps cannot be skipped: installing the old lock on the new interpreter produces a rejection in one of three cases.
A project supporting more than one interpreter at once is read from the same table too. In
the measurement, every interpreter has its own lock: three locks, 2 distinct sets.
Supporting two interpreters at once means keeping two locks, not fitting a single one to
both. A single lock only covers interpreters sharing every version’s compatibility set — in
the table, Y2 and Y3 are such a pair, Y1 and Y3 are not.
Standing side by side has a cost too, and it sits in the same table: a separate site, install, and lock for every supported interpreter. All three are paid separately for every promise the project makes; as supported versions grow, so does the number of sets to pin down, and each one waits to be validated on its own.
Summary
- The interpreter is a dependency too, and it narrows the candidate pool; 2 distinct version sets are born without the declaration, registry, or strategy changing.
- The distinct-outcome count depends not on how many environments there are but on the compatibility table: three interpreters give two sets because two of them allow the same candidates.
- A lock produced on one interpreter can be rejected on another: 6 of nine installs are accepted, 3 rejected. Rejection is not an error, it is the lock’s correct behavior.
- A lock guarantees reproducibility within one interpreter, not across the interpreter axis; on that axis it is a constraint, not a guarantee.
- Supporting two interpreters at once means not fitting one lock to both, it means keeping two locks; a single lock covers only interpreters sharing a common compatibility set.
- An interpreter switch is rebuilding the environment, and the lock is regenerated; its cost is not installation, it is validating the new set.
Next Step
Across this topic, four axes were measured — the isolation boundary, the resolution strategy, the resolver itself, and the interpreter version — and all four were made uniform by the same method: make the decision once, write it down, have later runs read it. How many distinct outcomes an install gives can now be counted.
What remains is the actual thing inside the install: the code. Once the environment has been brought down to a single set, does the code running in that set give the same decision on every run? The next topic asks that question, and to measure the answer, it takes up what a test is, what a suite declares, and how many distinct decisions the same suite can give.
To keep your progress and take notes, Log in
My notes
Log in to take notes.