datetime.tzinfo
You rarely instantiate tzinfo yourself — timezone and zoneinfo.ZoneInfo are ready-made subclasses. Write one only for rules neither can express. fromutc is the hook astimezone() uses to go from UTC to local time.
Demo
from datetime import datetime, timedelta, timezone, tzinfo class Fixed(tzinfo): def utcoffset(self, dt): return timedelta(hours=3) def tzname(self, dt): return 'MSK' def dst(self, dt): return timedelta(0) dt = datetime(2026, 9, 29, 12, tzinfo=Fixed()) (dt.isoformat(), dt.tzname(), dt.astimezone(timezone.utc).isoformat())
Nothing is checked when the datetime is created — the offset is validated when something asks for it (here isoformat()), so +30 hours fails with "offset must be a timedelta strictly between ...". fromutc simply adds the offset for a timezone, which is why 23:00 UTC on 9999-12-31 at +02:00 overflows.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| dt | datetime | None | yes | utcoffset/dst/tzname receive the datetime being asked about (None when called from a time). fromutc receives a datetime whose fields are UTC and whose tzinfo is self. |
Return value
tzinfo — Instances are passed as tzinfo= to datetime and time; the datetime calls back into them.
Attributes
| Attribute | Type | Meaning |
|---|---|---|
| utcoffset(dt) | timedelta | None | Total offset from UTC, DST included; strictly within ±24 h. None means "unknown" and makes the datetime naive. |
| dst(dt) | timedelta | None | The DST part of that offset (timedelta(0) outside DST). Used by timetuple() for tm_isdst and by the default fromutc(). |
| tzname(dt) | str | None | A display name such as "CEST" — what %Z prints. |
| fromutc(dt) | datetime | Given dt with UTC fields and tzinfo=self, return the local equivalent. The base version uses utcoffset() and dst(); timezone overrides it with dt + offset. |
Common patterns
from datetime import timedelta, tzinfo class Fixed(tzinfo): def __init__(self, hours, name): self._offset = timedelta(hours=hours) self._name = name def utcoffset(self, dt): return self._offset def dst(self, dt): return timedelta(0) def tzname(self, dt): return self._name
from datetime import tzinfo def localize(dt, tz): if not isinstance(tz, tzinfo): raise TypeError("tz must be a tzinfo") return dt.astimezone(tz)
Examples
Pitfalls
from datetime import datetime, timedelta, tzinfo class Fixed(tzinfo): def utcoffset(self, dt): return timedelta(hours=3) def dst(self, dt): return timedelta(0) Fixed().fromutc(datetime(2026, 9, 29, 9, tzinfo=Fixed()))
from datetime import datetime, timedelta, tzinfo class Fixed(tzinfo): def utcoffset(self, dt): return timedelta(hours=3) def dst(self, dt): return timedelta(0) tz = Fixed() tz.fromutc(datetime(2026, 9, 29, 9, tzinfo=tz)).hour
from datetime import datetime, timedelta, tzinfo class Half(tzinfo): def utcoffset(self, dt): return timedelta(hours=1) def tzname(self, dt): return 'X' datetime(2026, 9, 29, tzinfo=Half()).strftime('%Z')
from datetime import datetime, timedelta, tzinfo class Whole(tzinfo): def utcoffset(self, dt): return timedelta(hours=1) def tzname(self, dt): return 'X' def dst(self, dt): return timedelta(0) datetime(2026, 9, 29, tzinfo=Whole()).strftime('%Z')
When to use
- Type hints and isinstance checks for "any time zone object"
- Rules neither timezone (fixed) nor zoneinfo (IANA database) can express
- Fixed offsets → timezone
- Real-world regions → zoneinfo.ZoneInfo
- Writing your own DST rules for real regions — they change by law; use the tz database
Notes
FAQ
For a fixed offset use timezone(timedelta(hours=...), name) — no subclass needed. For a region use zoneinfo.ZoneInfo("Europe/Berlin"). Subclass tzinfo only for custom rules, implementing utcoffset, dst and tzname.