I opened a second shell and expected the same three-line repro to pass, because the laptop had already agreed with me. Have you ever copied a working snippet onto a cleaner machine and watched the import fail for a reason that felt rude? I wanted these notes to answer a smaller question before any suggested patch was allowed to vote. Which interpreter actually ran the file, and which package versions did that process believe were already installed?
The laptop carried a virtual environment, a lockfile, and a shell alias that quietly preferred one Python over another. The second machine mattered only if I treated it as another runtime, not as a prettier copy of the laptop. I am not claiming a measured outage, a customer incident, or a vendor benchmark anywhere in these notes. The mismatch later on is a constructed repro I would force, so the habit gets practiced before a real failure arrives.
Why a clean host still lies
A fresh server can look healthy while python3 still points at the system interpreter sitting outside your virtual environment. You install the project, the prompt returns, and you assume the lockfile was honored rather than merely glanced at. Did the remote installer resolve a new tree, or did it install the exact set you hashed beside the laptop tests? I would not let a drafting model answer that until both hosts had written the same receipt to disk.
Where a free server and a free model fit
MonkeyCode is relevant here only because a free server can host the second run and free model access can draft the mismatch note. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I would not hide that relationship under a tutorial voice, and I would not stretch it into a claim about quality. The only product facts I am using are operator-supplied: free model access exists, and a free server option exists.
I would park the second run on that free server and ask the free model to summarize receipt diffs, not to invent pins. If those options changed later, the script and the decision table would still be the part I would keep. A vanished option would change convenience, not the question of whether the two interpreters were ever the same. Have you kept a check that still runs after the assistant you used to draft the note is gone?
The receipt I write before the argument
I wanted one JSON document per host, small enough to paste into notes and stable enough to diff without a narrative. The script records the executable, the version token, the platform string, and whether a virtual environment is active. It also hashes a few well-known lockfile names when they exist, and it keeps only environment keys that affect imports. I omit secrets and full environment dumps on purpose, because a receipt should be safer to share than a shell history.
The following scripts are unexecuted examples I would review locally before I pointed them at any shared host.
#!/usr/bin/env python3
'''Write an interpreter receipt as JSON. Unexecuted example, not a measured run.'''
import hashlib
import json
import os
import platform
import sys
from importlib import metadata
from pathlib import Path
TRACKED = ('pip', 'setuptools', 'urllib3', 'certifi', 'tzdata')
ENV_KEYS = ('VIRTUAL_ENV', 'PYTHONPATH', 'PYTHONHOME', 'PYTHONDONTWRITEBYTECODE')
LOCK_NAMES = ('requirements.txt', 'requirements.lock', 'Pipfile.lock')
def file_sha256(path):
if not path.is_file():
return None
digest = hashlib.sha256()
digest.update(path.read_bytes())
return digest.hexdigest()
def package_versions(names):
found = {}
for name in names:
try:
found[name] = metadata.version(name)
except metadata.PackageNotFoundError:
found[name] = None
return found
def build_receipt():
root = Path.cwd()
return {
'executable': sys.executable,
'version': sys.version.split()[0],
'implementation': platform.python_implementation(),
'platform': platform.platform(),
'prefix': sys.prefix,
'base_prefix': sys.base_prefix,
'in_venv': sys.prefix != sys.base_prefix,
'packages': package_versions(TRACKED),
'env': {key: os.environ.get(key) for key in ENV_KEYS},
'lock_sha256': {name: file_sha256(root / name) for name in LOCK_NAMES},
}
def main():
json.dump(build_receipt(), sys.stdout, indent=2, sort_keys=True)
sys.stdout.write('\n')
if __name__ == '__main__':
main()
Read the tracked package tuple as a starting list, not as a universal bill of materials for every project you maintain. I would add the libraries my repro actually imports, then freeze that tuple so both hosts record the same names. A missing package becomes null rather than an exception, which is what I want when the remote image never installed it. Would you rather crash the receipt, or see a null that makes the absence obvious in the later diff?
What the script intentionally leaves out
The script does not record CPU count, disk size, or a cloud image name, because I was not given those facts to assert. It does not sample network reachability, and it will not tell you whether a wheel was built from source or downloaded. I would rather omit a field than print a guess that looks like a measurement taken from the second host. If your outage lives in one of those omitted fields, this workflow will look green and still be wrong.
Commands I type on each host
I keep the command list short so the second day starts with repetition instead of a fresh theory.
python3 env_receipt.py > local-receipt.json
# copy env_receipt.py, diff_receipts.py, and the lockfile to the second host
python3 env_receipt.py > remote-receipt.json
python3 diff_receipts.py local-receipt.json remote-receipt.json
I run the script with the same interpreter I intend to blame, because a receipt from the wrong binary is another story. On the second host I copy the script and the lockfile first, then I generate the remote JSON before I open a chat. The diff helper exits non-zero when a compared field disagrees, so a wrapper can stop before any patch is applied. I would commit both JSON files next to the note, since a terminal screenshot is a poor artifact for the next morning.
#!/usr/bin/env python3
'''Diff two receipt files. Unexecuted helper; exit 1 when a compared field differs.'''
import json
import sys
from pathlib import Path
COMPARE = ('executable', 'version', 'implementation', 'in_venv', 'prefix')
def load_receipt(path):
return json.loads(Path(path).read_text(encoding='utf-8'))
def compare(left, right):
rows = []
for key in COMPARE:
lv, rv = left.get(key), right.get(key)
rows.append((key, lv, rv, lv == rv))
left_pkgs = left.get('packages') or {}
right_pkgs = right.get('packages') or {}
for name in sorted(set(left_pkgs) | set(right_pkgs)):
lv, rv = left_pkgs.get(name), right_pkgs.get(name)
rows.append(('pkg:' + name, lv, rv, lv == rv))
return rows
def main():
if len(sys.argv) != 3:
sys.stderr.write('usage: diff_receipts.py left.json right.json\n')
return 2
rows = compare(load_receipt(sys.argv[1]), load_receipt(sys.argv[2]))
failed = False
for key, lv, rv, ok in rows:
mark = 'ok' if ok else 'DIFF'
failed = failed or not ok
print('%s\t%s\t%r\t%r' % (mark, key, lv, rv))
return 1 if failed else 0
if __name__ == '__main__':
sys.exit(main())
The constructed repro I would force
I would create a virtual environment on the laptop, skip that step on the server, and install without the lockfile. Locally, sys.prefix should differ from sys.base_prefix, while the server receipt should show those two paths as equal. A tracked name such as urllib3 or tzdata can then show a version on one side and null on the other. If application code is edited while those fields already disagree, whose bug did we just decorate with a confident patch?
An illustrative fixture, not a capture from any host I measured, only shows the shape I want the diff to expose.
{
"base_prefix": "/usr",
"executable": "/usr/bin/python3",
"in_venv": false,
"packages": {
"certifi": null,
"tzdata": null,
"urllib3": null
},
"prefix": "/usr",
"version": "UNMEASURED"
}
I would treat a version field written as UNMEASURED as a sign that the fixture is documentation, not a live capture. The useful signal in that shape is a cluster of null packages beside an executable that never entered a virtual environment. I would compare that shape to the laptop receipt before I let anyone rewrite the calling code. If the only live difference is a null tzdata, I install the missing package and recapture, rather than rewriting the caller.
What broke when I rushed the comparison
The first failure I expect is not library logic, but a receipt captured by a different python3 than the failing command. You then possess a tidy JSON file that documents the wrong process, and the project looks guilty by association. The second failure is a lockfile that never left the laptop, so every remote hash is null and the tool looks broken. I would copy the missing file, rerun both commands, and only then ask whether the versions themselves still disagree.
Aliases that make the wrong binary look chosen
which python and which python3 can disagree with sys.executable once the environment scripts directory is only half activated. I would print sys.executable from the same process that runs the repro, rather than trusting a parent shell's idea of python. A shell alias can make an interactive session pass while a non-interactive runner uses the system binary and fails the import. Have you checked whether the failing command was started by a script, a scheduler, or your own fingers on the keyboard?
Resolver drift the receipt can only hint at
pip install without hashes can select a newer transitive wheel on the server than the laptop resolved on an earlier day. I would not call that a model failure, because the model never saw the resolver choices unless I pasted both receipts. The hash field tells me whether the input file matched, not whether the installer was invoked with a require-hashes flag. If I care about that flag, I add the exact install command to the note, since the JSON does not record shell history.
How I would talk to the drafting model
I paste the diff output with a short instruction, and I forbid a code change until the two runtimes are comparable. The prompt below is a template I would edit, not a transcript from a session I already ran.
Two interpreter receipts are below. Do not propose an application patch.
List only keys that differ, including package nulls.
If executable or in_venv differs, stop and say the runtimes are not comparable.
Do not invent package versions, image names, quotas, or hardware.
Have you noticed how quickly a suggestion rewrites application code when the prompt never mentioned the executable path? I would reject any draft that edits source while executable or in_venv still differs, even if the prose sounds careful. The model is only a scribe for the mismatch here, and the receipt script remains the check that can fail a build.
A table I keep beside the diff
| Signal | What I conclude | What I do next |
|---|---|---|
| executable differs | These runs are not the same program | Recapture both receipts with the intended binary |
| in_venv differs | One side never entered the environment | Activate or create it, then recapture |
| version differs | Language behavior may not match | Pin the interpreter before debating library code |
| package null on one side | That import environment is incomplete | Install from the same lock, then recapture |
| only lock hash differs | The input file diverged before install | Copy the intended lock and reinstall |
| compared keys match | The environment is not the obvious cause | Inspect inputs, clocks, or the network next |
This table is a decision aid for a constructed check, not a catalog of every production incident I have already closed. I would add a row only after a real mismatch taught me a new signal, and I would date that row in the note. If two rows fire at once, I handle the interpreter and the virtual environment before I touch package versions. Otherwise I end up pinning a library inside a process that was never the process under test.
What I would repeat on the second day
- I generate both receipts before I open a model chat or accept a suggested edit from either host.
- I diff the two JSON files with a helper that exits non-zero as soon as one compared field disagrees.
- I repair the environment until those keys match, or I write down why they are allowed to differ.
- I ask for a plain mismatch note, then I rerun the original repro with the executable named in the receipt.
- I save both JSON files beside the note so nobody reconstructs the story from memory tomorrow.
The repeatable part is the order of the checks, not a promise that a free server will look identical next week. I would rather repeat a dull capture than repeat an argument about whose laptop counted as the normal machine. If the repro still fails after the receipts match, I finally have permission to suspect the code instead of the host. That permission is the whole point of spending two days on notes, and it arrives later than my impatience wants.
Who should not bother
You should skip this if the bug depends on a GPU, a private network, or data that cannot leave a controlled lab. A JSON receipt will not prove data residency, quota, image permanence, or any hardware shape I have not measured. Teams that already build from a hermetic image and a hash-checked lock may only need the executable identity check. I would also skip the model step entirely when policy forbids pasting environment details into a hosted assistant.
The note I would leave myself
I write the receipt, diff it, and only then ask for one sentence that explains the mismatch in plain words. If a MonkeyCode account is already available, the free server can hold the second run and the free model can draft that sentence. I would still read the diff and rerun the script myself before I merged a claim that the two hosts matched.
Top comments (0)