Das Symptom
Der wöchentliche Jour fixe stand seit Monaten auf neun Uhr. Am ersten Mittwoch
nach der Zeitumstellung steht er auf zehn – bei manchen Teilnehmern, bei anderen
weiterhin auf neun. Zwei Leute erscheinen, drei nicht, und alle drei haben den
Kalender aufgeschlagen, bevor sie losgegangen sind.
Andere Ausprägungen derselben Sache: Eine Einladung aus dem Ausland liegt beim
Empfänger um mehrere Stunden daneben. Ein Serientermin fällt an genau einem
Sonntag im März ersatzlos aus. Ein nächtlicher Sicherungslauf, der um 2:30 Uhr
starten sollte, ist an einem Tag im Jahr einfach nicht gelaufen, ohne
Fehlermeldung.
Die „Layer 8“-Wahrheit
Gespeichert wurde nicht, was gemeint war. Gemeint war „neun Uhr in Berlin“ –
gespeichert wurde entweder eine nackte Uhrzeit ohne Ortsangabe oder ein fester
Zeitpunkt in Weltzeit. Beides ist etwas anderes, und beides fällt erst auf, wenn
die Sommerzeit dazwischengeht.
RFC 5545, der Standard hinter jeder ICS-Datei und jeder Einladung per Mail, kennt
genau drei Formen. Eine Ortszeit ohne Zeitzonenbezug heißt dort „floating“ und ist
ausdrücklich an keine Zeitzone gebunden: Zwei Eingeladene in verschiedenen Ländern
nehmen dann zu verschiedenen realen Zeitpunkten am selben Termin teil. Weltzeit
ist eindeutig, aber sie beschreibt einen Zeitpunkt, keine Regel – „jeden Mittwoch
um neun Uhr in Berlin“ entspricht im Winter einem anderen UTC-Wert als im Sommer.
Nur die dritte Form, Ortszeit mit Zeitzonenbezug, hält beides zusammen. Der
Standard sagt das selbst und schreibt für den festen Zeitpunkt ein MUST vor.
Der Layer-8-Anteil ist der Reflex, danach am Kalenderprogramm zu drehen. Das
Programm hat nichts falsch gemacht: Es hat den gespeicherten Zeitpunkt korrekt
wiederholt und im Frühjahr sogar korrekt einen Termin übersprungen, weil es die
Stunde von 2 bis 3 Uhr an diesem Tag nicht gibt – auch das steht so im Standard.
Reparieren lässt sich das nicht an der Anzeige, sondern nur an der Speicherung:
Zonenname statt Stundenversatz, TZID statt nackter Uhrzeit, und in der Einladung
drei zusätzliche Wörter für die Menschen, die den Kalender ohnehin nicht
aufmachen.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.Die Einladung im Klartext ansehen
Die ICS-Datei ist eine Textdatei. Öffne die Einladung aus der Mail mit einem Editor und sieh dir die Zeile DTSTART an. Steht dort eine nackte Uhrzeit, ist der Termin nach RFC 5545 eine schwebende Zeit und bedeutet für jeden Empfänger etwas anderes. Ein angehängtes Z bedeutet Weltzeit, ein vorangestelltes TZID bedeutet Ortszeit mit Zeitzonenbezug – nur die letzte Form überlebt eine Zeitumstellung unbeschadet.
DTSTART:20260325T090000 # schwebend – gilt überall anders
DTSTART:20260325T080000Z # Weltzeit – fester Zeitpunkt
DTSTART;TZID=Europe/Berlin:20260325T090000 # richtig für Serientermine
2.Den eigenen Fall nachrechnen, statt zu raten
Bevor jemand den Kalender verdächtigt, lässt sich die Verschiebung in fünf Zeilen zeigen. Der Serientermin liegt fest in Weltzeit; die Berliner Ortszeit springt trotzdem, weil dazwischen die Sommerzeit beginnt. Genau diese Ausgabe ist der Beweis, dass nicht das Programm gerechnet hat, sondern die Zeitzone.
python3 -c "
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
B, U = ZoneInfo('Europe/Berlin'), ZoneInfo('UTC')
start = datetime(2026, 3, 25, 9, 0, tzinfo=B).astimezone(U)
for w in range(3):
t = start + timedelta(weeks=w)
print(t.date(), 'UTC', t.strftime('%H:%M'), '-> Berlin', t.astimezone(B).strftime('%H:%M %Z'))
"
# 2026-03-25 UTC 08:00 -> Berlin 09:00 CET
# 2026-04-01 UTC 08:00 -> Berlin 10:00 CEST
# 2026-04-08 UTC 08:00 -> Berlin 10:00 CEST
3.Zeitzonen als Namen speichern, nicht als Stundenversatz
Ein Versatz wie +01:00 ist eine Momentaufnahme, kein Ort. Europe/Berlin dagegen ist eine Regel, die weiß, wann umgestellt wird – auch wenn sich die Regel ändert, was politisch regelmäßig vorkommt. In Datenbanken, Konfigurationen und Schnittstellen gehört deshalb der Zonenname hinein. Wer nur den Versatz speichert, hat die Sommerzeit fest eingebrannt und merkt es zweimal im Jahr.
4.Die doppelte Stunde im Herbst prüfen
Am Umstellungswochenende im Oktober gibt es 2:30 Uhr zweimal. Python macht die Zweideutigkeit über das Merkmal `fold` sichtbar: derselbe Ortszeit-Wert, zwei verschiedene reale Zeitpunkte, eine Stunde auseinander. RFC 5545 entscheidet sich hier für das erste Vorkommen; jedes Programm, das etwas anderes tut, weicht bewusst oder unbewusst ab.
python3 -c "
from datetime import datetime
from zoneinfo import ZoneInfo
B, U = ZoneInfo('Europe/Berlin'), ZoneInfo('UTC')
for f in (0, 1):
a = datetime(2026, 10, 25, 2, 30, tzinfo=B, fold=f)
print('fold', f, a, '| Offset', a.utcoffset(), '| UTC', a.astimezone(U))
"
# fold 0 2026-10-25 02:30:00+02:00 | Offset 2:00:00 | UTC 2026-10-25 00:30:00+00:00
# fold 1 2026-10-25 02:30:00+01:00 | Offset 1:00:00 | UTC 2026-10-25 01:30:00+00:00
5.Nachts nichts zwischen 2 und 3 Uhr planen
Die Stunde, die im Frühjahr ausfällt, nimmt alles mit, was in ihr liegt: Sicherungen, Abrechnungen, Erinnerungen, Schichtwechsel. Nach RFC 5545 müssen Serientermine mit nicht existierender Ortszeit übersprungen werden. Wer den nächtlichen Lauf auf 1:30 oder 3:30 Uhr legt, umgeht das Problem vollständig und ohne Sonderregel.
6.Bei geplanten Terminen die Zeitzone mitschreiben – auch im Text
In der Einladung, im Betreff und in der Beschreibung gehört die Zone dazu: neun Uhr Berliner Zeit, nicht neun Uhr. Das kostet drei Wörter und ersetzt die halbe Stunde Rückfragen, wenn zwei Teilnehmer in verschiedenen Ländern sitzen. Für den Menschen ist der Satz die Absicherung gegen einen falsch konfigurierten Kalender – die Maschine hält sich an TZID, der Mensch an den Text.
7.Naive Zeitstempel aus dem eigenen Code verbannen
In Protokollen und Datenbanken ist ein Zeitstempel ohne Zone die häufigste Ursache derselben Verwirrung. Die Python-Dokumentation formuliert es scharf: Ein naives Objekt „does not contain enough information to unambiguously locate itself relative to other date/time objects“ – ob es Weltzeit, Ortszeit oder etwas anderes meint, hänge allein vom Programm ab. Beim Speichern also Weltzeit plus Zonenname, beim Anzeigen erst umrechnen.
python3 -c "
from datetime import datetime, timezone
print(datetime(2026, 3, 29, 2, 30).utcoffset())
print(datetime.now(timezone.utc).isoformat(timespec='seconds'))
"
# None