Skip to content
About
Main content

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

30 January plus one month then one day, against plus one day then one month: the same date?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    the-daylight-saving-key-bug.py40 linesDownload
    View 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)
    
  3. 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

02:30 on 26 October 2025, Europe/Berlin
CaseLocal textUTC
02:30, first time (fold=0)2025-10-26 02:302025-10-26T00:30:00+00:00
02:30, second time (fold=1)2025-10-26 02:302025-10-26T01:30:00+00:00
Reference runApple M2 · 8 cores · 8 GB
Output · uv run the-daylight-saving-key-bug.py
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-28
macOS 26.6.2 · Python 3.13.3 · python-dateutil 2.9.0.post0 · run 2026-10-20

What we found

Both had the local text 2025-10-26 02:30 and were 1 hour apart in (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 , so define the order once in a shared function rather than in each query.