The daylight-saving key bug
Measure your machineNeeds Python
Problem
Local time is not an identifier. When clocks go back, an hour of local times happens twice, so a key built from local wall-clock text collides for two different instants. Calendar arithmetic has its own trap: months have different lengths, so adding a month and a day gives different dates depending on the order, because "plus one month" is clamped to the end of a shorter month.
Show that 26 October 2025, 02:30 in Berlin has two instants (fold 0 and fold 1) with identical local text that differ by one hour. Then show that 30 Jan + 1 month + 1 day differs from 30 Jan + 1 day + 1 month with your library's month rule (here dateutil.relativedelta).
The question. Do the two 02:30s have the same local text, how far apart are they, and does the order of month and day arithmetic matter?
Predict
The local text is identical but the instants differ by one hour. 30 Jan + 1 month + 1 day differs from 30 Jan + 1 day + 1 month.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
the-daylight-saving-key-bug.py # /// script # requires-python = ">=3.10" # dependencies = ["python-dateutil"] # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """The daylight-saving key bug. 26 October 2025, 02:30 in Berlin happens twice. The two instants share the same local text but are an hour apart, so a key built from local time collides. Then month arithmetic: 30 January + 1 month + 1 day against 30 January + 1 day + 1 month, with dateutil's month rule. """ import argparse, json from datetime import datetime, date, timedelta from zoneinfo import ZoneInfo from dateutil.relativedelta import relativedelta p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() berlin, utc = ZoneInfo("Europe/Berlin"), ZoneInfo("UTC") a = datetime(2025, 10, 26, 2, 30, tzinfo=berlin, fold=0); b = datetime(2025, 10, 26, 2, 30, tzinfo=berlin, fold=1) local = [a.strftime("%Y-%m-%d %H:%M"), b.strftime("%Y-%m-%d %H:%M")] instants = [a.astimezone(utc).isoformat(), b.astimezone(utc).isoformat()] d = date(2025, 1, 30) m_then_d = d + relativedelta(months=1) + timedelta(days=1); d_then_m = d + timedelta(days=1) + relativedelta(months=1) rows = [{"case": "02:30, first time (fold=0)", "local_text": local[0], "utc": instants[0]}, {"case": "02:30, second time (fold=1)", "local_text": local[1], "utc": instants[1]}] summary = {"same_local_text": "yes" if local[0] == local[1] else "no", "hours_apart": (b.astimezone(utc) - a.astimezone(utc)).total_seconds() / 3600, "month_then_day": m_then_d.isoformat(), "day_then_month": d_then_m.isoformat()} for r in rows: print(f"{r['case']:<28} local {r['local_text']} UTC {r['utc']}") print(f"same local text: {summary['same_local_text']}; {summary['hours_apart']:.0f} hour apart") print(f"30 Jan + 1 month + 1 day = {m_then_d}; 30 Jan + 1 day + 1 month = {d_then_m}") if args.json: with open(args.json, "w") as f: json.dump({"summary": summary, "tables": {"instants": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run the-daylight-saving-key-bug.py uv run the-daylight-saving-key-bug.py --help # every knob
Our run
02:30 on 26 October 2025, Europe/Berlin
| Case | Local text | UTC |
|---|---|---|
| 02:30, first time (fold=0) | 2025-10-26 02:30 | 2025-10-26T00:30:00+00:00 |
| 02:30, second time (fold=1) | 2025-10-26 02:30 | 2025-10-26T01:30:00+00:00 |
02:30, first time (fold=0) local 2025-10-26 02:30 UTC 2025-10-26T00:30:00+00:00
02:30, second time (fold=1) local 2025-10-26 02:30 UTC 2025-10-26T01:30:00+00:00
same local text: yes; 1 hour apart
30 Jan + 1 month + 1 day = 2025-03-01; 30 Jan + 1 day + 1 month = 2025-02-28What we found
Both had the local text 2025-10-26 02:30 and were 1 hour apart in UTC (2025-10-26T00:30:00+00:00 and 2025-10-26T01:30:00+00:00). 30 January plus a month plus a day was 2025-03-01; plus a day plus a month, 2025-02-28.
Read it
Store and key by instants (UTC or an offset-qualified timestamp) and convert to local time only for display; a local-time key loses or merges an hour of data twice a year. Calendar arithmetic is not associative, so define the order once in a shared function rather than in each query.