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.

datetime classPython 2.3+Live demo
Common call
tz.fromutc(utc_dt.replace(tzinfo=tz))
Returns
the same instant as local wall time, tzinfo=tz
Replaces
Offset arithmetic scattered through the code
Watch out
The base methods raise NotImplementedError; fromutc requires dt.tzinfo is self
class MyZone(tzinfo)
→ tzinfo

Demo

Live evaluation
A minimal fixed-rule tzinfo. The datetime calls utcoffset and tzname on it whenever it needs them.
Try:
Inputs
hoursintoffset in hours
namestra zone name
Code
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())
Result
('2026-09-29T12:00:00+03:00', 'MSK', '2026-09-29T09:00:00+00:00')

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

NameTypeRequiredDescription
dtdatetime | Noneyesutcoffset/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

AttributeTypeMeaning
utcoffset(dt)timedelta | NoneTotal offset from UTC, DST included; strictly within ±24 h. None means "unknown" and makes the datetime naive.
dst(dt)timedelta | NoneThe DST part of that offset (timedelta(0) outside DST). Used by timetuple() for tm_isdst and by the default fromutc().
tzname(dt)str | NoneA display name such as "CEST" — what %Z prints.
fromutc(dt)datetimeGiven 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

Minimal fixed-offset subclass
What timezone does for you — shown for the protocol.
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
Accept any tzinfo in an API
timezone, ZoneInfo and custom subclasses are all tzinfo instances.
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

1. A custom subclass in use
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())
Returns
('2026-09-29T12:00:00+03:00', 'MSK', '2026-09-29T09:00:00+00:00')
2. The base class is abstract
from datetime import tzinfo tzinfo().utcoffset(None)
Returns
NotImplementedError: a tzinfo subclass must implement utcoffset()
3. timezone is a tzinfo
from datetime import tzinfo, UTC isinstance(UTC, tzinfo)
Returns
True
4. timezone.fromutc adds the offset
from datetime import datetime, timedelta, timezone tz = timezone(timedelta(hours=2)) tz.fromutc(datetime(2026, 9, 29, 12, tzinfo=tz))
Returns
datetime.datetime(2026, 9, 29, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)))
5. fromutc insists on its own tzinfo
from datetime import datetime, timedelta, timezone tz = timezone(timedelta(hours=2)) tz.fromutc(datetime(2026, 9, 29, 12))
Returns
ValueError: fromutc: dt.tzinfo is not self
6. Bad return types are caught
from datetime import datetime, tzinfo class Broken(tzinfo): def utcoffset(self, dt): return 3 datetime(2026, 9, 29, tzinfo=Broken()).utcoffset()
Returns
TypeError: tzinfo.utcoffset() must return None or timedelta, not 'int'

Pitfalls

1. Calling fromutc with another instance
The check is identity (is), not equality: a second Fixed() is a different object.
new instance
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()))
ValueError: fromutc: dt.tzinfo is not self
same object
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
12
2. Leaving dst() unimplemented
timetuple() — and therefore strftime() — asks for dst(). Implement all three methods even if dst is always timedelta(0).
no dst()
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')
NotImplementedError: a tzinfo subclass must implement dst()
with dst()
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')
'X'

When to use

Use it
  • Type hints and isinstance checks for "any time zone object"
  • Rules neither timezone (fixed) nor zoneinfo (IANA database) can express
Reach for something else
  • 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

CPython impl
tzinfo is a C type in Modules/_datetimemodule.c; its utcoffset/dst/tzname raise NotImplementedError, fromutc implements the documented algorithm
Validation
utcoffset() and dst() results are checked each time they are used: None or a timedelta strictly within ±24 h
Identity
astimezone(tz) returns self unchanged when self.tzinfo is tz; fromutc requires dt.tzinfo is self
Pickling
A subclass needs an __init__ that can be called with no arguments (or a __reduce__) to be picklable

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.