Have you ever merged a helper because a remote run stayed green while your editor still looked calm? I almost did that this week, and then my laptop failed the same snapshot before I finished coffee. The JSON trees looked identical until I stopped reading keys and started reading punctuation instead of structure. What actually moved was a timestamp that quietly inherited whatever timezone the process happened to live in.
Hour 0 through 4: a boring helper with a hidden clock
I needed a tiny snapshot writer for a webhook fixture, nothing glamorous, and nothing that deserved a war story. An agent draft looked clean enough, so I let it emit generated_at with datetime.now().isoformat() and called the file done. Why would I question a clock call that every tutorial still shows on the first page? The remote job printed a green line, and I treated that green line like evidence instead of a hint.
# stamp_snapshot.py — first draft I should not have trusted
from datetime import datetime
import json
from pathlib import Path
def write_snapshot(payload: dict, path: Path) -> None:
envelope = {
"generated_at": datetime.now().isoformat(),
"payload": payload,
}
path.write_text(json.dumps(envelope, indent=2) + "\n", encoding="utf-8")
That function is short, readable, and wrong for any test that compares snapshot files as raw text. Naive datetime.now() follows the process timezone, and isoformat() then freezes that hometown into a string. Two honest machines can serialize the same event into two different snapshots without either process crashing loudly.
Hour 5 through 12: I blamed everything except the clock
I started with the usual superstitions because they had saved me before, which is a terrible reason to keep using them. Did pytest cache a previous failure under .pytest_cache, and was I about to spend a night on that ghost? Was PYTHONHASHSEED shuffling dict order on one interpreter and leaving the other one stably sorted for no good reason? I ran the same commands in both places and wrote the output into a notes file like a person who still believed in coincidence.
Commands I actually ran while the mismatch still felt supernatural:
python -c "import sys; print(sys.version)"
python -c "import os; print(repr(os.environ.get('PYTHONHASHSEED')))"
python -c "import json; print(json.dumps({'b': 1, 'a': 2}, sort_keys=True))"
pytest -vv tests/test_stamp_snapshot.py --cache-clear
diff -u tests/goldens/webhook.json /tmp/webhook.json
The versions matched closely enough that I stopped caring about patch levels for the rest of the night. Hash seed was unset in both shells, and sort_keys=True did not change the mismatch at all. diff kept pointing at one line, the generated_at line, while I kept staring at the payload as if the payload were lying.
I even stripped microseconds because I have seen clocks disagree about six digits on a busy CI worker. The hour value and the missing offset still disagreed, which should have been the only clue I needed. Why did I keep editing the payload instead of asking which city the clock thought it was in?
Hour 13 through 20: the remote box was not my desk
This is where the remote box enters the notes, and I want that entrance to stay boring and factual.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
I had used MonkeyCode's free model access to draft the helper after I got tired of typing boilerplate envelopes. Then I ran the first green check on the free server option so my laptop could keep compiling something else. That split was convenient, and it was also the entire bug hiding in plain sight. The free server process followed UTC, while my laptop followed the office timezone, and the snapshot format never named either one.
I am not going to invent model names, quotas, or hardware details I cannot verify from this desk. What I can verify is the Python behavior, which you can reproduce on any two machines whose TZ values differ. The product mattering in this story is only that a free remote shell is a different computer, not a mirror of your desk.
python - <<'PY'
from datetime import datetime, timezone
import time
print("tzname", time.tzname)
print("naive", datetime.now().isoformat())
print("aware", datetime.now(timezone.utc).isoformat())
print("local-aware", datetime.now().astimezone().isoformat())
PY
Run that snippet under TZ=UTC and then under TZ=America/Los_Angeles if you want the mismatch without any product involved. Do you see how the naive strings stop being interchangeable the moment the zone changes under your feet? I did not see it until hour twenty, which is late for a one-line clock bug with no stack trace.
TZ=UTC python stamp_demo.py
TZ=America/Los_Angeles python stamp_demo.py
date -u
date
datetime.utcnow() is the other trap I almost copied from an old snippet during this stretch. CPython 3.12 deprecated that helper because it returns a naive object that pretends to be UTC. The replacement I am using here is datetime.now(timezone.utc), which keeps the offset attached to the value.
Hour 21 through 32: a reproducible artifact I will keep
I wanted a check that fails on purpose when the clock is naive, not a sermon about UTC. The following module is the thing I wish I had pasted before the agent run, and you can drop it into a throwaway directory. It writes two snapshots, one naive and one UTC-aware, then prints them as text so diff has something honest to chew on.
# clock_field_notes.py
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
import json
import os
import time
def naive_stamp() -> str:
return datetime.now().isoformat()
def utc_stamp() -> str:
return datetime.now(timezone.utc).isoformat()
def envelope(stamp: str, payload: dict) -> dict:
return {
"generated_at": stamp,
"tzname": list(time.tzname),
"payload": payload,
}
def dump(path: Path, stamp: str, payload: dict) -> None:
path.write_text(
json.dumps(envelope(stamp, payload), indent=2) + "\n",
encoding="utf-8",
)
def main() -> None:
payload = {"event": "invoice.paid", "amount": 1900}
root = Path("artifacts")
root.mkdir(exist_ok=True)
dump(root / "naive.json", naive_stamp(), payload)
dump(root / "utc.json", utc_stamp(), payload)
print("TZ", os.environ.get("TZ"))
print("naive", (root / "naive.json").read_text(encoding="utf-8"))
print("utc", (root / "utc.json").read_text(encoding="utf-8"))
if __name__ == "__main__":
main()
Then I wrapped a pytest around the rule I actually care about: golden files must not contain naive timestamps. This is an executable example, not a customer suite, and you should treat the fixture names as local scratch.
# test_clock_field_notes.py
from datetime import datetime, timezone
from pathlib import Path
import json
import re
from clock_field_notes import dump, utc_stamp
OFFSET = re.compile(r"(Z|[+-]\d{2}:\d{2})$")
def test_golden_timestamp_is_timezone_aware(tmp_path: Path) -> None:
path = tmp_path / "golden.json"
dump(path, utc_stamp(), {"event": "invoice.paid"})
stamp = json.loads(path.read_text(encoding="utf-8"))["generated_at"]
parsed = datetime.fromisoformat(stamp)
assert parsed.tzinfo is not None
assert OFFSET.search(stamp)
assert parsed.utcoffset() == timezone.utc.utcoffset(parsed)
If you want the naive version to fail, swap utc_stamp() for naive_stamp() and watch tzinfo come back empty. That failing test is the artifact. The passing test is only the control, and I keep both in the notes so the next 48 hours start shorter.
Commands I would repeat on a new machine
- Print
TZ,time.tzname, and bothisoformatstrings before you touch fixtures or goldens. - Run the same script under two explicit
TZvalues and diff the files as ordinary text. - Parse
generated_atwithdatetime.fromisoformatand asserttzinfois notNone. - Freeze time in unit tests if the golden file must stay byte-stable across reruns.
from datetime import datetime, timezone
from unittest.mock import patch
# Sketch: freeze an aware instant when the golden must be byte-stable.
FROZEN = datetime(2026, 9, 12, 18, 0, 0, tzinfo=timezone.utc)
def test_frozen_utc_is_byte_stable(tmp_path):
with patch("clock_field_notes.utc_stamp", return_value=FROZEN.isoformat()):
...
I am labeling that last block as a sketch, not as a production suite I ran against a customer clock. Freeze the clock when the file must be a golden. Keep the clock live when you are proving timezone awareness, because a frozen naive datetime will hide the bug you came here to catch.
Hour 33 through 44: what broke, in plain language
Three things broke, and only one of them was Python itself.
- The helper stored a hometown clock instead of an instant with an offset.
- The remote shell and the laptop disagreed about that hometown without raising anything.
- I treated a green remote run as a proof of reproducibility instead of a second computer.
The JSON payload never drifted. Key order never drifted. Microseconds were a distraction I built for myself after the first diff. Once I forced datetime.now(timezone.utc) and required an offset in the golden regex, both machines wrote the same shape even when their civil clocks disagreed.
Python 3.9 already ships zoneinfo in the standard library, so you do not need a third-party zone database for this particular check. I still prefer timezone.utc for goldens because I want one canonical offset, not a travelogue of office cities. If your golden files were minted with naive strings, migrate them in one commit so the diff stays honest and reviewable.
A small decision table I taped above the desk
| Signal in the golden | What it usually means | What I do next |
|---|---|---|
| No offset, hour jumps by 7 or 8 | Naive local time leaked into the file | Rewrite the stamp with timezone.utc
|
| Offset present, bytes still drift | Unfrozen clock in CI or on a laptop | Patch time or drop the field from the golden |
Offset present, only Z versus +00:00
|
Formatter disagreement, not a zone bug | Normalize the suffix before compare |
| Entire file drifts, keys included | Different serializer settings or key order | Pin sort_keys and separators |
Would I still trust diff -u after this? Yes, but only after the stamp is aware and the test refuses naive strings.
What I would repeat, and who should skip this
I would still let an agent draft the boring envelope code, because that part is typing, not judgment. I would not let the first green remote run retire the laptop run, especially when the artifact is a text snapshot. I would export TZ=UTC in the test job, and I would keep a two-zone replay in the notes for anything that serializes time.
A repeatable loop that actually earned its place in the notebook looks like this:
- Generate the helper, then immediately print naive and aware stamps on that same machine.
- Replay the writer under
TZ=UTCand under one non-UTC zone before you keep any golden. - Assert
tzinfoon parse, not equality of wall-clock hour strings. - Only then compare files, and only after the clock is either frozen or removed.
This approach is a poor fit if your timestamps are display strings for humans in a single office. It is also a poor fit if you already hash payloads and ignore metadata clocks on purpose. Do not drag UTC into a calendar-date feature that must follow civil local time, because that is a different bug with a different test and a different kind of anger.
Limitations I actually hit are boring, which is why they belong here. Freezing time hides zone bugs if you freeze a naive datetime and then congratulate yourself. Regexing offsets will reject some legal ISO-8601 forms you have not seen yet, especially odd colon-less variants. datetime.fromisoformat also will not parse every RFC 3339 trick, so keep the producer and the consumer on the same helper instead of mixing libraries mid-test.
If you need a second machine that is not your laptop timezone, I parked this check on MonkeyCode's free server option after the local assertion already existed.
Top comments (0)