Knowing the protocol explains DST behaviour and lets you model unusual offsets, such as historical or business-defined zones.
tzinfo is an abstract base class. A datetime holds a reference to a tzinfo and asks it for information whenever needed:
utcoffset(dt)total offset from UTC as a timedelta (including DST)dst(dt)the DST part of that offset, orNonetzname(dt)a display name like"CET"fromutc(dt)called byastimezone: given a datetime holding UTC fields withtzinfo=self, return the local equivalent
The methods receive the naive wall-clock fields plus fold. For a zone with transitions, utcoffset must decide from local time, which is ambiguous in folds; that's why fold exists. fromutc goes the other way, which is unambiguous, and must set fold=1 on the second occurrence of a repeated hour.
Implementations must be immutable and hashable, should implement __reduce__ or be picklable, and must be cheap because they're called often. datetime.timezone covers fixed offsets; ZoneInfo reads compiled TZif files and binary-searches transition tables, caching instances per key.
Writing your own is rarely needed. Reasons do exist: zones defined by business rules (a market's trading calendar), tests that need fake transitions, or historical data with local mean time offsets. In those cases subclass tzinfo and test the gap and fold behaviour carefully.
Example
from datetime import UTC, datetime, timedelta, tzinfo
class FakeZone(tzinfo):
"""UTC+1, switching to UTC+2 from 1 April at 02:00 local (no fall-back)."""
def utcoffset(self, dt):
return timedelta(hours=1) + self.dst(dt)
def dst(self, dt):
switch = datetime(dt.year, 4, 1, 2)
return timedelta(hours=1) if dt.replace(tzinfo=None) >= switch else timedelta(0)
def tzname(self, dt):
return "FST" if self.dst(dt) else "FT"
def fromutc(self, dt):
local = dt + timedelta(hours=1) # try standard time first
if local.replace(tzinfo=None) >= datetime(dt.year, 4, 1, 2):
local += timedelta(hours=1)
return local
z = FakeZone()
print(datetime(2025, 3, 1, 12, tzinfo=z).isoformat()) # 2025-03-01T12:00:00+01:00
print(datetime(2025, 5, 1, 12, tzinfo=z).tzname()) # FST
utc = datetime(2025, 4, 1, 1, 30, tzinfo=UTC)
print(utc.astimezone(z).isoformat()) # 2025-04-01T03:30:00+02:00Remember
- A datetime asks its tzinfo for
utcoffset,dstandtznameusing local fields fromutcconverts from UTC and must handle folds by settingfold=1- Use
timezoneorZoneInfo; custom tzinfo is for unusual rules and tests
Watch out for
- Returning offsets that depend on mutable state; tzinfo objects must behave as immutable values.