Two New York 01:30s: equal Python values can represent different instants
Reproduce an explicit New York autumn 2026 fold in CPython 3.12.3: shared-tzinfo equality/subtraction versus UTC instants, distinct-object and ordinary-time controls, complete public fixture and actual run evidence. No recurrence parser or calendar product test.
Immutable revision: 88668cca9336e157e59fb954c0128417e38638517b02144efbe5a5051fb09691. Publication and independent validation are separate; inspect metadata and provenance before use.
Two New York 01:30s: equal Python values can represent different instants
A handoff saying only “November 1, 2026 at 01:30 in New York” leaves a real choice unresolved. In this fictional local experiment, the first occurrence is 05:30Z and the second 06:30Z. Yet CPython 3.12.3 returns equality True and a zero difference when the two aware values share the same tzinfo object. Do not treat that comparison as proof that the owner meant one instant.
Ask which occurrence is intended; retain the date, zone, fold/offset and resolved UTC instant together. For elapsed time or instant equality, this case converts both aware values to UTC before comparing. It does not choose a fold for an owner, deduplicate a real calendar or infer that two events have the same identity.
Python's datetime operations notes 3–5 ignore fold in same-object timezone comparisons and use wall-clock subtraction there. ZoneInfo's fold documentation chooses the pre-transition offset for fold=0 and post-transition offset for fold=1. IANA 2026c New York/US rules ground the expected offsets; the measured rows below include both sides of the transition.
| Supplied local date/time | Fold | Zone | Offset | UTC instant |
|---|---|---|---|---|
| 2026-11-01 00:30 | 0 | America/New_York | −04:00 | 2026-11-01 04:30Z |
| 2026-11-01 01:30 | 0 | America/New_York | −04:00 | 2026-11-01 05:30Z |
| 2026-11-01 01:30 | 1 | America/New_York | −05:00 | 2026-11-01 06:30Z |
| 2026-11-01 02:30 | 0 | America/New_York | −05:00 | 2026-11-01 07:30Z |
| 2026-11-02 01:30 | 0 | America/New_York | −05:00 | 2026-11-02 06:30Z |
The before/after wall-clock difference is two hours; the UTC elapsed difference is three. The ordinary November 2 control equals its UTC counterpart. During the repeated hour, raw equality to the corresponding UTC value is false despite zero subtraction. PEP 495's arithmetic/equality sections describe these separate rules.
Two fresh ZoneInfo.from_file objects loaded from identical bytes have the same key but distinct object identities. The checker shows that different-fold subtraction then gives 3600 seconds; same-fold equality can still be false with zero subtraction. This is a diagnostic control, not a suggested cache or timezone-object workaround. Both-UTC comparisons preserve the intended instant distinction.
The fixture also rejects ordinary-time fold=1 through its own round-trip validation. Python permits constructing that value; setting fold=1 alone does not establish ambiguity (PEP 495 fold attribute). Missing/invalid folds, another zone, spring-gap input, offset-bearing local input, invalid clock and wrong row scope/order have separate wrapper guards. Spring gaps are excluded before conversion, not resolved by this experiment.
The earlier four-Sundays case [cited revision] rejects ambiguous inputs, and the spring-gap provider case [cited revision] excludes folds. This case supplies explicit fold interpretation and comparison; it performs no recurrence expansion or RFC5545 parsing.
Reproduce the local result
Use CPython 3.12.3 on Linux with /usr/share/zoneinfo/tzdata.zi labeled 2026c. The checker requires the New York TZif file's exact 3552 bytes and SHA256 recorded below. System zone data is an external dependency; a different build can have equivalent rules but different bytes and needs a separately recorded comparison. The currently inspected Python 3.12 documentation identifies edition 3.12.15; that is documentation, not the executed runtime.
Save the five named fences into one empty disposable directory, preserving UTF-8 without BOM, LF and exactly one final LF. Run python3 check.py > replay-report.json. It writes observed.json, compares every output field and byte with the declared expected.json, and exercises ten scope/shape guards. Success exits 0 with empty main stderr; guard invocations exit 2 without partial stdout. Inspect the report before interpreting a mismatch; do not relabel missing/different data as a successful replay. Cleanup is deleting the disposable directory after keeping desired outputs.
The actual run record separates its execution time, runtime, zone bytes, field comparisons and error outcomes from the expected payload. Since observed bytes exactly equal expected bytes, that one complete fence supplies both payload locators. Report timestamps change on replay; its digest identifies this captured run. A digest proves neither execution nor source authenticity. No account, invitation, calendar import or live-event action occurs.
Case profile
{
"schema": "calendar-case-v1",
"tags": [
"calendar",
"recurrence",
"timezone"
],
"product": "Local CPython datetime/zoneinfo fold comparison; no calendar service",
"client_version": "CPython 3.12.3; standard-library datetime and zoneinfo",
"api_or_ui": "python3 check.py > replay-report.json",
"checked_at": "2026-10-05T09:26:18.281432Z",
"tzdb_version": "Observed system tzdb label 2026c; exact New York TZif bytes pinned by checker",
"timezone_id": "America/New_York",
"intent": "wall-time",
"input_fixture": {
"description": "Five supplied fictional local timestamps: before, both 01:30 folds, after the November 1 transition and one ordinary November 2 control. Explicit folds; UTC-normalized fixed-instant comparison. No recurrence rule, expansion, exclusions or event updates.",
"icalendar_text": null,
"reproduction_command": "python3 check.py > replay-report.json",
"manifest": [
{
"file": "fixture.json",
"fence": "fixture.json",
"format": "JSON",
"encoding": "UTF-8 without BOM; LF; exactly one final LF",
"bytes": 379,
"sha256": "345e919ccd0368f93cebd771e45f83691cdafed99442b660395e3608bda02715"
},
{
"file": "observe.py",
"fence": "observe.py",
"format": "Python",
"encoding": "UTF-8 without BOM; LF; exactly one final LF",
"bytes": 3712,
"sha256": "100d76222c3b525e2b420ab28afa4040bf4db6fa0c50037b52a4caa39dfc227f"
},
{
"file": "check.py",
"fence": "check.py",
"format": "Python",
"encoding": "UTF-8 without BOM; LF; exactly one final LF",
"bytes": 4236,
"sha256": "ce37aa8c7a50616bf4ab91d45c80444f1641367321deccd6d0283ac02a0ebe44"
},
{
"file": "expected.json",
"fence": "expected.json",
"format": "JSON",
"encoding": "UTF-8 without BOM; LF; exactly one final LF",
"bytes": 2316,
"sha256": "5c52af75c98168d34612e19c0b13233bd988fa48411cf712b0cfb28f221e692a"
},
{
"file": "test-report.json",
"fence": "test-report.json",
"format": "JSON",
"encoding": "UTF-8 without BOM; LF; exactly one final LF",
"bytes": 2139,
"sha256": "545e00daafa02bee4b009296abf066d619c3cd80058e67b5b5c9a779f8962366"
}
]
},
"expected_instances": [
{
"payload_fence": "expected.json",
"format": "JSON object",
"encoding": "UTF-8 without BOM; LF; exactly one final LF",
"size_bytes": 2316,
"sha256": "5c52af75c98168d34612e19c0b13233bd988fa48411cf712b0cfb28f221e692a",
"scope": "instances: five ordered objects with label/local_datetime/timezone_id/fold/utc_offset/utc_instant; comparisons: six named objects with same_tzinfo_object/raw_equal/raw_difference_seconds/utc_equal/utc_difference_seconds",
"basis": "Hand-authored IANA 2026c arithmetic and documented datetime/PEP 495 comparison rules, informed by the disclosed research probe; fixed before this final checker run."
}
],
"observed_instances": [
{
"payload_fence": "expected.json",
"format": "JSON object",
"encoding": "UTF-8 without BOM; LF; exactly one final LF",
"size_bytes": 2316,
"sha256": "5c52af75c98168d34612e19c0b13233bd988fa48411cf712b0cfb28f221e692a",
"scope": "All 30 instance-field values and 30 comparison-field values; complete actual output bytes equal expected.json",
"actual_run_fence": "test-report.json",
"actual_output_file": "observed.json",
"actual_execution_finished_at": "2026-10-05T09:26:18.281432Z",
"equality": "Exact bytes and every field equal expected.json; separate actual execution record below."
}
],
"test_status": "locally-reproduced",
"source_locators": [
{
"url": "https://docs.python.org/3.12/library/datetime.html#datetime-objects",
"locator": "Supported operations notes 3–5; datetime.astimezone"
},
{
"url": "https://docs.python.org/3.12/library/zoneinfo.html",
"locator": "Using ZoneInfo fold; ZoneInfo.from_file; Data sources"
},
{
"url": "https://peps.python.org/pep-0495/#temporal-arithmetic-and-comparison-operators",
"locator": "Temporal Arithmetic; Aware datetime Equality; fold invalid instances"
},
{
"url": "https://data.iana.org/time-zones/tzdb-2026c/northamerica",
"locator": "US 2007 max November Sun>=1 02:00 and final America/New_York -5:00 US continuation"
}
],
"limitations": [
"Only the five supplied November 1–2 2026 New York rows and six comparisons were positively measured; other accepted clocks on those dates are unmeasured.",
"Ten wrapper guards express this fixture scope and validation policy; they are not calendar or Python constructor rejection rules.",
"Spring gap inputs excluded by date guard before interpretation; no gap resolution, recurrence expansion, RFC5545 parser, serialized ICS, product import or invitation test.",
"Runtime3.12.3 differs from current Python 3.12.15 documentation page edition; measured behavior is version-bound.",
"Matching system TZif bytes and label required; external tzdb is not embedded. Different runtime/build/data needs a new recorded run."
],
"refresh_policy": "Recheck before November 1 transition, by 2026-11-01T00:00:00Z, and after runtime/tzdb/source or fixture changes."
}
| File / fence | Bytes | SHA256 |
|---|---|---|
fixture.json |
379 | 345e919ccd0368f93cebd771e45f83691cdafed99442b660395e3608bda02715 |
observe.py |
3712 | 100d76222c3b525e2b420ab28afa4040bf4db6fa0c50037b52a4caa39dfc227f |
check.py |
4236 | ce37aa8c7a50616bf4ab91d45c80444f1641367321deccd6d0283ac02a0ebe44 |
expected.json |
2316 | 5c52af75c98168d34612e19c0b13233bd988fa48411cf712b0cfb28f221e692a |
test-report.json |
2139 | 545e00daafa02bee4b009296abf066d619c3cd80058e67b5b5c9a779f8962366 |
fixture.json
{"timezone_id":"America/New_York","rows":[{"label":"before","local_datetime":"2026-11-01T00:30:00","fold":0},{"label":"first","local_datetime":"2026-11-01T01:30:00","fold":0},{"label":"second","local_datetime":"2026-11-01T01:30:00","fold":1},{"label":"after","local_datetime":"2026-11-01T02:30:00","fold":0},{"label":"ordinary","local_datetime":"2026-11-02T01:30:00","fold":0}]}
observe.py
"""Bounded fictional New York fold comparison, not a calendar parser."""
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo
ZONE = "America/New_York"
ZONE_FILE = Path("/usr/share/zoneinfo/America/New_York")
UTC = timezone.utc
def require(ok, reason):
if not ok:
raise ValueError(reason)
def pairs(items):
result = {}
for key, value in items:
require(key not in result, "duplicate-field")
result[key] = value
return result
def load_zone():
with ZONE_FILE.open("rb") as source:
return ZoneInfo.from_file(source, key=ZONE)
def value(row, zone):
require(isinstance(row, dict) and
set(row) == {"label", "local_datetime", "fold"}, "row-shape")
require(isinstance(row["label"], str) and bool(row["label"]), "label")
local = row["local_datetime"]
require(isinstance(local, str) and
re.fullmatch(r"2026-11-0[12]T[0-9]{2}:[0-9]{2}:[0-9]{2}", local),
"date-scope")
require(type(row["fold"]) is int and row["fold"] in (0, 1), "fold")
naive = datetime.fromisoformat(local)
result = naive.replace(tzinfo=zone, fold=row["fold"])
back = result.astimezone(UTC).astimezone(zone)
require(back.replace(tzinfo=None) == naive and back.fold == row["fold"],
"fold-not-valid")
return result
def compare(left, right):
uleft, uright = left.astimezone(UTC), right.astimezone(UTC)
return {"same_tzinfo_object": left.tzinfo is right.tzinfo,
"raw_equal": left == right,
"raw_difference_seconds": int((right-left).total_seconds()),
"utc_equal": uleft == uright,
"utc_difference_seconds": int((uright-uleft).total_seconds())}
def run(data):
require(isinstance(data, dict) and
set(data) == {"timezone_id", "rows"}, "fixture-shape")
require(data["timezone_id"] == ZONE, "zone-scope")
require(isinstance(data["rows"], list) and len(data["rows"]) == 5,
"row-count")
zone = load_zone()
values = [value(row, zone) for row in data["rows"]]
rows = []
for supplied, actual in zip(data["rows"], values):
rows.append({"label": supplied["label"],
"local_datetime": actual.replace(tzinfo=None).isoformat(),
"timezone_id": ZONE, "fold": actual.fold,
"utc_offset": actual.isoformat()[-6:],
"utc_instant": actual.astimezone(UTC).isoformat().replace("+00:00", "Z")})
before, first, second, after, ordinary = values
require([row["label"] for row in rows] ==
["before", "first", "second", "after", "ordinary"], "row-order")
other_zone = load_zone()
other_first = first.replace(tzinfo=other_zone)
other_second = second.replace(tzinfo=other_zone)
return {"instances": rows,
"comparisons": {
"folds_shared_object": compare(first, second),
"folds_distinct_same_key_objects": compare(first, other_second),
"same_instant_distinct_same_key_objects": compare(first, other_first),
"fold_to_own_utc": compare(first, first.astimezone(UTC)),
"ordinary_to_own_utc": compare(ordinary, ordinary.astimezone(UTC)),
"before_to_after": compare(before, after)}}
if __name__ == "__main__":
try:
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"),
object_pairs_hook=pairs)
output = run(data)
except (ValueError, OSError) as error:
print("REJECT: " + str(error), file=sys.stderr)
sys.exit(2)
print(json.dumps(output, indent=2))
check.py
"""Check complete fictional output and bounded reject cases locally."""
import copy
import hashlib
import json
import platform
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
BASE = Path(__file__).resolve().parent
ZONE_FILE = Path("/usr/share/zoneinfo/America/New_York")
ZONE_SHA = "e9ed07d7bee0c76a9d442d091ef1f01668fee7c4f26014c0a868b19fe6c18a95"
started = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
zone_bytes = ZONE_FILE.read_bytes()
if len(zone_bytes) != 3552 or hashlib.sha256(zone_bytes).hexdigest() != ZONE_SHA:
raise RuntimeError("different TZif bytes: record a separate comparison")
tzdb = Path("/usr/share/zoneinfo/tzdata.zi").read_text().splitlines()[0]
if tzdb != "# version 2026c":
raise RuntimeError("different tzdb label: record a separate comparison")
result = subprocess.run([sys.executable, "observe.py", "fixture.json"],
cwd=BASE, capture_output=True)
expected = (BASE / "expected.json").read_bytes()
(BASE / "observed.json").write_bytes(result.stdout)
if result.returncode or result.stderr or result.stdout != expected:
raise RuntimeError("execution or expected-byte mismatch")
if json.loads(result.stdout) != json.loads(expected):
raise RuntimeError("field mismatch")
original = json.loads((BASE / "fixture.json").read_text())
changes = [
("ordinary-fold1", "row", "fold", 1, 4, "fold-not-valid"),
("missing-fold", "remove", "fold", None, 1, "row-shape"),
("bool-fold", "row", "fold", True, 1, "fold"),
("fold2", "row", "fold", 2, 1, "fold"),
("wrong-zone", "top", "timezone_id", "America/Chicago", None, "zone-scope"),
("spring-gap-excluded", "row", "local_datetime", "2026-03-08T02:30:00", 1, "date-scope"),
("offset-bearing-input", "row", "local_datetime", "2026-11-01T01:30:00-04:00", 1, "date-scope"),
("invalid-clock", "row", "local_datetime", "2026-11-01T25:30:00", 1, None),
("missing-row", "truncate", None, None, None, "row-count"),
("wrong-order", "row", "label", "second", 1, "row-order")]
guards = []
with tempfile.TemporaryDirectory() as folder:
for label, kind, key, value, index, reason in changes:
changed = copy.deepcopy(original)
if kind == "row":
changed["rows"][index][key] = value
elif kind == "remove":
del changed["rows"][index][key]
elif kind == "top":
changed[key] = value
else:
changed["rows"].pop()
target = Path(folder) / "negative.json"
target.write_text(json.dumps(changed) + "\n")
run = subprocess.run([sys.executable, "observe.py", str(target)],
cwd=BASE, capture_output=True)
error = run.stderr.decode().strip()
if run.returncode != 2 or run.stdout or not error.startswith("REJECT: "):
raise RuntimeError("failed guard: " + label)
if reason is not None and error != "REJECT: " + reason:
raise RuntimeError("wrong rejection: " + label)
guards.append({"case": label, "exit": run.returncode,
"stdout_bytes": len(run.stdout), "stderr": error})
report = {"started_at": started,
"finished_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"runtime": platform.python_implementation() + " " + platform.python_version(),
"libraries": "stdlib datetime and zoneinfo; no third-party modules",
"tzdb": tzdb, "zone_file": str(ZONE_FILE),
"zone_bytes": len(zone_bytes), "zone_sha256": ZONE_SHA,
"command": "python3 check.py > replay-report.json",
"child_command": "python3 observe.py fixture.json",
"exit": result.returncode, "stderr_bytes": len(result.stderr),
"observed_bytes": len(result.stdout),
"observed_sha256": hashlib.sha256(result.stdout).hexdigest(),
"comparison": "All six fields of five ordered rows and all five fields of six named comparisons match expected.json; full UTF-8 bytes also equal",
"negative_cases": guards,
"status": "locally-reproduced; no recurrence expansion, parser or calendar product test"}
print(json.dumps(report, indent=2))
expected.json
{
"instances": [
{
"label": "before",
"local_datetime": "2026-11-01T00:30:00",
"timezone_id": "America/New_York",
"fold": 0,
"utc_offset": "-04:00",
"utc_instant": "2026-11-01T04:30:00Z"
},
{
"label": "first",
"local_datetime": "2026-11-01T01:30:00",
"timezone_id": "America/New_York",
"fold": 0,
"utc_offset": "-04:00",
"utc_instant": "2026-11-01T05:30:00Z"
},
{
"label": "second",
"local_datetime": "2026-11-01T01:30:00",
"timezone_id": "America/New_York",
"fold": 1,
"utc_offset": "-05:00",
"utc_instant": "2026-11-01T06:30:00Z"
},
{
"label": "after",
"local_datetime": "2026-11-01T02:30:00",
"timezone_id": "America/New_York",
"fold": 0,
"utc_offset": "-05:00",
"utc_instant": "2026-11-01T07:30:00Z"
},
{
"label": "ordinary",
"local_datetime": "2026-11-02T01:30:00",
"timezone_id": "America/New_York",
"fold": 0,
"utc_offset": "-05:00",
"utc_instant": "2026-11-02T06:30:00Z"
}
],
"comparisons": {
"folds_shared_object": {
"same_tzinfo_object": true,
"raw_equal": true,
"raw_difference_seconds": 0,
"utc_equal": false,
"utc_difference_seconds": 3600
},
"folds_distinct_same_key_objects": {
"same_tzinfo_object": false,
"raw_equal": false,
"raw_difference_seconds": 3600,
"utc_equal": false,
"utc_difference_seconds": 3600
},
"same_instant_distinct_same_key_objects": {
"same_tzinfo_object": false,
"raw_equal": false,
"raw_difference_seconds": 0,
"utc_equal": true,
"utc_difference_seconds": 0
},
"fold_to_own_utc": {
"same_tzinfo_object": false,
"raw_equal": false,
"raw_difference_seconds": 0,
"utc_equal": true,
"utc_difference_seconds": 0
},
"ordinary_to_own_utc": {
"same_tzinfo_object": false,
"raw_equal": true,
"raw_difference_seconds": 0,
"utc_equal": true,
"utc_difference_seconds": 0
},
"before_to_after": {
"same_tzinfo_object": true,
"raw_equal": false,
"raw_difference_seconds": 7200,
"utc_equal": false,
"utc_difference_seconds": 10800
}
}
}
test-report.json
{
"started_at": "2026-10-05T09:26:17.970102Z",
"finished_at": "2026-10-05T09:26:18.281432Z",
"runtime": "CPython 3.12.3",
"libraries": "stdlib datetime and zoneinfo; no third-party modules",
"tzdb": "# version 2026c",
"zone_file": "/usr/share/zoneinfo/America/New_York",
"zone_bytes": 3552,
"zone_sha256": "e9ed07d7bee0c76a9d442d091ef1f01668fee7c4f26014c0a868b19fe6c18a95",
"command": "python3 check.py > replay-report.json",
"child_command": "python3 observe.py fixture.json",
"exit": 0,
"stderr_bytes": 0,
"observed_bytes": 2316,
"observed_sha256": "5c52af75c98168d34612e19c0b13233bd988fa48411cf712b0cfb28f221e692a",
"comparison": "All six fields of five ordered rows and all five fields of six named comparisons match expected.json; full UTF-8 bytes also equal",
"negative_cases": [
{
"case": "ordinary-fold1",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: fold-not-valid"
},
{
"case": "missing-fold",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: row-shape"
},
{
"case": "bool-fold",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: fold"
},
{
"case": "fold2",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: fold"
},
{
"case": "wrong-zone",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: zone-scope"
},
{
"case": "spring-gap-excluded",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: date-scope"
},
{
"case": "offset-bearing-input",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: date-scope"
},
{
"case": "invalid-clock",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: hour must be in 0..23"
},
{
"case": "missing-row",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: row-count"
},
{
"case": "wrong-order",
"exit": 2,
"stdout_bytes": 0,
"stderr": "REJECT: row-order"
}
],
"status": "locally-reproduced; no recurrence expansion, parser or calendar product test"
}
Primary sources checked 2026-10-05T09:26:46Z; the local run finished 2026-10-05T09:26:18.281432Z. A distinct creator-controlled checker completed a separate replay of all five public files, ten public guards, five additional executable boundary checks and the eight editorial decisions. The baseline replay completed at 2026-10-05T09:34:52.422066Z; additional executable checks completed at 2026-10-05T09:36:04.970839Z. Same-controller checks supply no independent formal review or calendar-product result. This follows the current calendar standard [current article]. Recheck by 2026-11-01T00:00:00Z, before the transition, or after source, runtime, tzdb or input changes. Feedback should name the exact fictional row, comparison, runtime and local result without private event details.