Have you ever watched a one-line file read pass in your editor and then die the moment a clean process started it? I did, and I spent most of the first day blaming the sample file instead of the process that opened it. The JSON was valid, the permissions looked fine, and the command string matched in both terminals. What I had not checked was the working directory each process actually inherited before that open ran.
The failure I kept renaming
The job itself was small enough to hide this class of mistake: load a sample file, validate three fields, and write a short summary. Locally, python -m jobs.load_sample printed the summary and exited zero, which felt like proof that the loader was done. On a clean start, the same module raised FileNotFoundError before it parsed a single field or wrote any summary. I kept asking whether the file had been gitignored, renamed, or saved under a slightly different extension.
A directory listing inside the repository answered no, and that should have ended the file-name theory sooner. Was the file missing, or was I looking for it from the wrong directory the whole time? I still treated the editor task as the real environment, because that task had never made me type cd myself. The runner on a clean machine does not owe you the folder your IDE happened to open last.
The first wrong fix was the one my fingers already knew: paste an absolute path from the laptop into the loader and rerun. That path existed only on my machine, so the clean job failed again with a different missing location. A green local run had only proved that my shell was already standing in the repository when the open happened. Would a second machine have accepted that absolute path, or was I freezing one laptop into the source tree?
Three facts the traceback did not volunteer
FileNotFoundError tells you the string that failed, and it does not tell you why that string was reasonable on one desk. I wanted three facts from the failing process, not a fresh theory built from memory of the editor session. Where did the process start, which file was executing, and which path did the loader compute before the open? Those three lines are dull on purpose, and they would have saved the first evening of renamed theories.
A context print I now treat as mandatory
Label: the snippets in this note are a complete worked example. I am not pasting private run output, timings, or a production incident clock. The forty-eight hours are the shape of the note, meaning the order I would repeat, not a measured outage.
from pathlib import Path
import os
import sys
def debug_context(target: Path) -> dict[str, str]:
return {
"cwd": os.getcwd(),
"file": str(Path(__file__).resolve()),
"argv0": sys.argv[0],
"target": str(target),
"target_exists": str(target.is_file()),
}
I printed that mapping once from the editor task and once from a shell that had not entered the repo. The editor cwd was the repository root, so the relative sample path resolved by accident rather than by design. The other process kept its start directory, and the same relative string pointed at a folder with no sample. The relative path was never a property of the package, and it belonged only to whoever launched the process.
The repair I would actually commit
I am calling this a worked reproduction rather than a production outage, because the checks below are the only evidence I want you to trust. There is no timing claim, no hidden dataset, and no second bug hiding in a private log I cannot show. The sample file is three fields, and the only interesting behavior is where the path comes from when the process starts. If you cannot rerun a sentence in this note, treat that sentence as a suggestion rather than a result.
Place this file at jobs/data/sample.json, next to the loader, so the data location can follow the module instead of the shell.
{
"name": "sample",
"version": 1,
"status": "ready"
}
File-relative resolution
import json
from pathlib import Path
PACKAGE_ROOT = Path(__file__).resolve().parent
def sample_path() -> Path:
"""Resolve sample data from this file, not from the process cwd."""
return PACKAGE_ROOT / "data" / "sample.json"
def load_sample() -> dict:
path = sample_path()
if not path.is_file():
raise FileNotFoundError(
f"missing sample at {path}; cwd={Path.cwd()}"
)
return json.loads(path.read_text(encoding="utf-8"))
if __name__ == "__main__":
print(json.dumps(load_sample(), sort_keys=True))
Why did the editor task feel so much smarter than this small module when both of them called the same function? It had already selected a working directory, and it never showed that choice next to the command you thought you ran. A file-relative path makes that hidden help unnecessary for data that ships beside the package and should travel with it. If the command must read whatever ./data the user points at, skip this helper and document cwd as part of the contract.
A test that leaves the repository
The test changes directory before the call, which is the whole point of keeping it next to the loader. If I forget and assert against a relative string, this test should fail in CI rather than on a stranger's laptop. I also want a negative check that the old string still depends on cwd, so I remember why the helper exists. This second test is optional, and it should stay only while you are still teaching the trap to yourself.
from pathlib import Path
from jobs.load_sample import load_sample, sample_path
def test_load_sample_ignores_foreign_cwd(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
payload = load_sample()
assert payload["name"] == "sample"
assert sample_path().is_file()
assert not (tmp_path / "data" / "sample.json").exists()
def test_naive_relative_string_follows_cwd(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
naive = Path("data") / "sample.json"
assert not naive.is_file()
Commands that keep me honest
I do not want a note that says the loader worked without the command that made that sentence true. These are the commands I would paste above the function, and I would rerun them after any path edit. You will need jobs/__init__.py if your interpreter does not treat jobs as a package yet. I would rather add that file on purpose than discover an import error and blame the path helper for it.
# fail the old habit on purpose, from a directory that is not the repo
cd /tmp && python -m jobs.load_sample
# after the file-relative fix, the same foreign cwd should pass
cd /tmp && python -m jobs.load_sample
# the test, also from a foreign cwd, so pytest's start folder is not the proof
cd /tmp && python -m pytest tests/test_load_sample.py -q
Run the module as a module, not as a loose script, so the import path matches the clean job. A loose python jobs/load_sample.py can still look fine in an editor and then disagree with python -m once the start directory changes. Would you notice that difference if the only green button you trusted was the one inside the IDE?
Where a free model and a free server fit
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
In this workflow I use MonkeyCode's free model access for one narrow job, and only after the traceback and the cwd printout already exist. I ask it to turn those notes into a reproducer checklist, not into a story about an outage I cannot show. The useful lines are questions about cwd, __file__, a directory-changing test, and a command started from /tmp. I would not trust a checklist I had not rerun, even when every sentence sounded like a careful review.
The free server option answers a different question than the model does, which is why I do not treat them as one tool. My editor, my shell startup, and a notebook kernel I still had open were all quietly donating state to the bug. A clean server process does not inherit that donation, so it is a fair place to run the foreign-cwd commands once. I treat that option as a second start directory, not as a copy of production and not as a permanent lab.
Recheck the current offer before you plan a workflow around it, because I was told about availability rather than hardware. I am not inventing a model name, a quota, a region, or a duration, since none of those were part of the claim. I also would not paste secrets, customer paths, or private dumps into a hosted model to get a cleaner paragraph. The sample in this note is synthetic, which is the only reason the path string is safe to show a model at all.
A small decision table
| Signal | Editor run | Clean server run | What I trust |
|---|---|---|---|
| Relative path opens | Often, if cwd is the repo | Often fails | The server, until a cwd-changing test exists |
| Laptop absolute path opens | Passes on one machine | Fails elsewhere | Neither result, the path is not portable |
Path(__file__) opens |
Passes from /tmp
|
Passes if the file shipped with the module | Both, after the foreign-cwd test is green |
| Model-written checklist | Fast to read | Irrelevant until executed | Only the lines I have rerun myself |
| One green process | Weak evidence | Stronger, still one sample | The test plus a second start directory |
The order I would write into the note
- First I reread the JSON and muttered about encoding, even though the exception was already
FileNotFoundError. - Next I confirmed the file was committed, then assumed the runner had cloned the wrong branch instead.
- Then I hardcoded the laptop path, watched the editor go green, and handed that same mistake to a clean start.
- The context print finally showed two different working directories, which made the relative string look late.
- Last I switched to
Path(__file__), added the foreign-cwd test, and reran the module from/tmp.
What would I repeat from that list, and what would I refuse to repeat even under a short deadline? I would repeat the context print and the hostile test, because both still work when I am tired and sure. I would refuse the absolute path, even if the demo was due before lunch and the editor button was already green.
What I would repeat
- Print cwd,
__file__, and the computed path before editing the loader at all. - Run the module from
/tmp, not only from the repository root your editor already chose. - Reject any fix that contains a personal absolute path, even when that path works this afternoon.
- Add one test that changes directory before the read, and keep it after the lesson sinks in.
- Use a clean server run as a second start directory, then keep the test so the server is not required forever.
- Ask a free model for a checklist only after the traceback is in hand, and rerun every line it suggests.
Limitations, and who should skip this
This approach does not prove production parity, and I do not want anyone to mistake one clean run for that proof. A free server can have a different Python build, a different mount layout, or a different start command than the pager host. One green clean run is still one sample, especially if the missing file appears only after a deploy step you forgot. If the failure needs a private network, a licensed dataset, or a queue you cannot recreate, stop and use a closer environment.
Skip the hosted model step if the traceback contains credentials, customer identifiers, or unpublished paths you cannot trim. Skip the free server if your team already has a locked runner that matches production more closely than a general option. Skip the file-relative helper when the file is user input that must stay relative to cwd, such as a documented CLI contract. In that case, set the working directory in the job spec and test that contract on purpose rather than hiding it.
There is another limit inside Path(__file__) itself that this note should not pretend to solve. A frozen app, a zip import, or a loader that runs from a stream may not have a normal sibling file on disk. If that is your packaging story, use the package data API and test that API instead of assuming a folder layout. I am not claiming a speedup or a saved hour count, because I did not measure those and they were not the bug.
The bug was a path that only looked stable because something else had already changed directory for me. If you already have a MonkeyCode account, run the foreign-cwd test once on the free server, then leave that test in the repo so the next clean machine does not depend on that session.
Top comments (0)