Have you ever stared at a perfectly reasonable patch and still watched the same exception roll by? I did, for two long days, on a small Python service that should have been easy. The function on disk looked corrected, my ad-hoc tests looked green, and the running process still called old code. Why would a runtime ignore a file I could open with my own eyes?
This 48-hour field note is about import identity, and it is not a story about model quality. I used a free remote server and a free coding model as extra hands, then wasted hours feeding them the wrong tree. The bug was boring once I printed it, which is usually how these stories end. Would you have printed module.__file__ from the live process before asking for a third rewrite?
What I thought was broken
I assumed the agent could not see recent edits, because every suggested patch described code I had already changed. I also assumed the free server was serving an old process, because restarts felt too cheap to distrust. I even assumed my local laptop was the source of truth, which is a flattering story and a bad measurement. Have you noticed how those three confident assumptions quietly skip the same uncomfortable measurement question?
The skipped question is simple, and I should have written it on a sticky note. Which file does this running process import when the code says import billing at startup? That answer is not the file git shows, and it is not the file your editor tab displays. Until that path is printed from the live interpreter, every patch is a guess dressed up as work.
Hours 0–8: I treated the model like a pair programmer
I cloned the service onto a free server so I could reproduce the failure away from my laptop's clutter. Then I asked a free model to read the traceback and propose a patch against the checkout. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option were extra hands in this loop, not a magic runtime.
The first patches still matched Tuesday
The first patch looked careful, with a renamed helper, a tighter None check, and a log line I wanted. I applied it, restarted the worker the way I always restart workers, and watched the same traceback land with the same line numbers. Did I verify those line numbers against the file on disk, or did I trust my memory of Tuesday's traceback? I trusted memory instead, and that memory was still quoting an older copy of the module.
I asked the model to try again, and it did, with a slightly different guard. I asked it to add a unit test against the checkout, and the generated test looked completely reasonable. The unit test imported billing and passed, which made me trust the tree even more. Passing tests can still import the wrong module if both copies share a name, and I forgot that rule.
Hours 8–24: I started blaming the free server
Was the process even running from this directory, or was I listing files in a reassuring but irrelevant tree? I listed pwd, I listed ls -l, and I listed git log -1 --oneline like a person performing competence. The commit hash matched my laptop, and the file contents matched my laptop as well. The running worker still behaved like last Tuesday, which should have been impossible given that hash.
Folk spells that did not change __file__
I killed the process and started it from the repo root with an explicit command.
# example: start from the checkout you believe is live
python -m billing.worker
The same exception came back immediately, with the same helper name I thought I had already removed. I set PYTHONPATH=. because that is the folk spell for this class of bug.
# example: folk spell, not a diagnosis
export PYTHONPATH="$(pwd)"
python -m billing.worker
The same exception came back after the folk spell, which should have ended my faith in environment variables. At that point I was ready to accuse the free server of caching bytecode in a place I could not see. Is stale bytecode still a reasonable suspect in 2026, or is that folklore from another decade? It is still a reasonable suspect sometimes, but it was not the villain in this case.
I deleted __pycache__ trees anyway, because deleting caches feels like progress when you are tired.
# example: I ran this more than once
find . -type d -name '__pycache__' -exec rm -rf {} +
The worker still imported fee logic I had already deleted from billing/fees.py in the checkout. A reasonable person would print __file__ at that moment, and then stop rearranging the furniture. I opened another editor tab instead of printing the path, which is an impressive way to waste an afternoon.
Hours 24–40: the receipt I should have printed first
The live process finally got a tiny probe I should have shipped at hour one. I injected a small helper that prints the imported module path, the head of sys.path, and the pip distribution. I ran that helper inside the worker's interpreter, because a receipt from any other Python is a second fiction. The script below is the whole artifact; it is boring on purpose, and that is the point.
# path_receipt.py — run this inside the same interpreter as the worker
from __future__ import annotations
import json
import os
import sys
from importlib import import_module, metadata
def receipt(modname: str) -> dict:
module = import_module(modname)
dist_file = None
dist_version = None
try:
dist = metadata.distribution(modname)
dist_version = dist.version
dist_file = str(dist.locate_file(""))
except metadata.PackageNotFoundError:
pass
return {
"modname": modname,
"module_file": getattr(module, "__file__", None),
"package_path": list(getattr(module, "__path__", [])),
"sys_path_head": sys.path[:8],
"executable": sys.executable,
"cwd": os.getcwd(),
"sys_path0": sys.path[0] if sys.path else None,
"dist_version": dist_version,
"dist_locate_file": dist_file,
}
if __name__ == "__main__":
name = sys.argv[1] if len(sys.argv) > 1 else "billing"
print(json.dumps(receipt(name), indent=2))
I ran it with the same executable as the worker, not with a different python I happened to have in PATH.
# example: pin the interpreter the worker actually uses
/usr/bin/python3 path_receipt.py billing
The printed module_file was not inside the git checkout I had been patching all day. It pointed at a site-packages/billing/ tree from an earlier pip install . that had copied the package into the environment. The agent had been editing a source tree that the running process never imported at all. How many hours did I spend reviewing diffs against a file that was not on sys.path?
That install was not an editable install, and it behaved like a frozen snapshot of last week's commit. Git was honest, pip was honest, and the model was honest about the files I actually showed it. I was the only person mixing those three honest reports into a single confusing narrative.
A decision table I now keep in the repo
I needed a boring table in the repo more than I needed another optimistic prompt. This is the check I run before I ask any model to touch import-related code.
| Observation | Likely cause | Do this next | Do not do this |
|---|---|---|---|
| Checkout matches git, runtime does not | Non-editable install shadowing the tree | Print module.__file__ from the worker |
Keep patching the checkout |
__file__ is under site-packages
|
pip install . or an old wheel |
Reinstall with pip install -e . or uninstall |
Set PYTHONPATH and hope |
__file__ is under the repo, behavior is old |
Stale process or stale .pyc
|
Restart by pid, then delete caches | Blame the model |
Laptop __file__ differs from server __file__
|
Environment drift | Sync install method, then re-run receipt | Copy patches between machines |
| Tests pass, worker fails | Tests and worker use different sys.path
|
Run tests with the worker's executable | Add more tests in the checkout only |
Would that table have saved me on day one, before I burned a second afternoon on theater? It would not have saved me from one optimistic patch, because I am stubborn about first attempts. It would have saved me from the next six patches, which is the only savings I care about now.
What I would repeat
I would still use a free remote server as a clean-ish reproduction box, because my laptop is a museum of half-installed tools. I would still use a free model to draft the probe, because writing importlib.metadata boilerplate from memory is a waste of a morning. I would not let the model propose a functional patch until the path receipt from the live interpreter is pasted into the thread.
The repeatable loop is short, and I now keep it above the prompt in my notes.
- Start the worker the same way production starts it, including the same executable.
- Run
path_receipt.pywith that executable and save the JSON next to the traceback. - Paste the receipt into the model thread before you paste any source file.
- If
module_fileis outside the checkout, fix the install method and stop editing. - Only then ask for a behavioral patch, and re-run the receipt after install changes.
# example: make the install match the checkout you want to debug
python -m pip uninstall -y billing
python -m pip install -e .
python path_receipt.py billing
If the new module_file still points at site-packages without a repo path, the distribution is still a snapshot. An editable install should resolve into your working tree, usually through a .pth file or a direct link. If it does not, you are still debugging the wrong copy, and the model cannot see that unless you show it.
Limitations, and who should skip this
This workflow assumes a CPython process you can restart and a package you are allowed to reinstall. It does not help when you cannot attach to the running interpreter or restart it cleanly. It also fails for code frozen by Nuitka, PyInstaller, or another bundler that hides real files. It also does not prove behavioral correctness, because it only proves which bytes got imported.
Skip this if a disciplined image build already installs from a lockfile and never bind-mounts a checkout. Skip it if the service is not Python, because module.__file__ will not be the lever you think. Skip it if you cannot tell which executable the worker uses, because the receipt from the wrong interpreter is a second lie. Skip asking a model to "just fix imports" when you have not printed __file__ from the worker.
That last prompt trains you to argue with a shadow copy, and the argument can last two days. I am not claiming the free server is fast, durable, or equivalent to a production rack. I am not claiming the free model understands packaging better than a boring pip show output. I am claiming that a JSON receipt is cheaper than a second day of plausible patches.
If you already have a free remote box, run the path dump before you ask any model to patch imports. Before I let any assistant edit a module, I make the process confess the path it imported. If that path is not the file in my editor, the conversation is over until the install is honest. Would you keep arguing with a model after that printout, or would you finally uninstall the ghost package?
Top comments (0)