6.9.6 tzinfo Objects
tzinfo is an abstract base clase, meaning that this class should not
be instantiated directly. You need to derive a concrete subclass, and (at least) supply
implementations of the standard tzinfo methods needed by the datetime methods you use. The datetime module does
not supply any concrete subclasses of tzinfo.
An instance of (a concrete subclass of) tzinfo can be passed to the
constructors for datetime and time objects. The
latter objects view their members as being in local time, and the tzinfo
object supports methods revealing offset of local time from UTC, the name of the time zone,
and DST offset, all relative to a date or time object passed to them.
Special requirement for pickling: A tzinfo subclass must have an __init__ method that can be called with no arguments, else it can be
pickled but possibly not unpickled again. This is a technical requirement that may be relaxed
in the future.
A concrete subclass of tzinfo may need to implement the following
methods. Exactly which methods are needed depends on the uses made of aware datetime
objects. If in doubt, simply implement all of them.
-
- Return offset of local time from UTC, in minutes east of UTC. If local time is west of
UTC, this should be negative. Note that this is intended to be the total offset from UTC;
for example, if a tzinfo object represents both time zone and DST
adjustments, utcoffset() should return their sum. If the UTC
offset isn't known, return
None. Else the value returned must be a timedelta object specifying a whole number of minutes in the range
-1439 to 1439 inclusive (1440 = 24*60; the magnitude of the offset must be less than one
day). Most implementations of utcoffset() will probably look like
one of these two:
return CONSTANT # fixed-offset class
return CONSTANT + self.dst(dt) # daylight-aware class
If utcoffset() does not return None, dst() should not return None either.
The default implementation of utcoffset() raises NotImplementedError.
-
- Return the daylight saving time (DST) adjustment, in minutes east of UTC, or
None
if DST information isn't known. Return timedelta(0) if DST is not in effect.
If DST is in effect, return the offset as a timedelta object (see utcoffset() for details). Note that DST offset, if applicable, has
already been added to the UTC offset returned by utcoffset(), so
there's no need to consult dst() unless you're interested in
obtaining DST info separately. For example, datetime.timetuple()
calls its tzinfo member's dst() method to
determine how the tm_isdst flag should be set, and tzinfo.fromutc() calls dst() to account for
DST changes when crossing time zones.
An instance tz of a tzinfo subclass that models both
standard and daylight times must be consistent in this sense:
tz.utcoffset(dt) - tz.dst(dt)
must return the same result for every datetime dt
with dt.tzinfo==tz For sane tzinfo
subclasses, this expression yields the time zone's "standard offset", which
should not depend on the date or the time, but only on geographic location. The
implementation of datetime.astimezone() relies on this, but cannot
detect violations; it's the programmer's responsibility to ensure it. If a tzinfo subclass cannot guarantee this, it may be able to override the
default implementation of tzinfo.fromutc() to work correctly with astimezone() regardless.
Most implementations of dst() will probably look like one of
these two:
return timedelta(0) # a fixed-offset class: doesn't account for DST
or
# Code to set dston and dstoff to the time zone's DST transition
# times based on the input dt.year, and expressed in standard local
# time. Then
if dston <= dt.replace(tzinfo=None) < dstoff:
return timedelta(hours=1)
else:
return timedelta(0)
The default implementation of dst() raises NotImplementedError.
-
- Return the time zone name corresponding to the datetime object dt,
as a string. Nothing about string names is defined by the datetime
module, and there's no requirement that it mean anything in particular. For example,
"GMT", "UTC", "-500", "-5:00", "EDT",
"US/Eastern", "America/New York" are all valid replies. Return
None
if a string name isn't known. Note that this is a method rather than a fixed string
primarily because some tzinfo subclasses will wish to return
different names depending on the specific value of dt passed, especially if the
tzinfo class is accounting for daylight time.
The default implementation of tzname() raises NotImplementedError.
These methods are called by a datetime or time
object, in response to their methods of the same names. A datetime
object passes itself as the argument, and a time object passes None
as the argument. A tzinfo subclass's methods should therefore be
prepared to accept a dt argument of None, or of class datetime.
When None is passed, it's up to the class designer to decide the best
response. For example, returning None is appropriate if the class wishes to say
that time objects don't participate in the tzinfo protocols. It may be
more useful for utcoffset(None) to return the standard UTC offset, as there is no
other convention for discovering the standard offset.
When a datetime object is passed in response to a datetime
method, dt.tzinfo is the same object as self. tzinfo
methods can rely on this, unless user code calls tzinfo methods
directly. The intent is that the tzinfo methods interpret dt
as being in local time, and not need worry about objects in other timezones.
There is one more tzinfo method that a subclass may wish to
override:
-
- This is called from the default datetime.astimezone()
implementation. When called from that,
dt.tzinfo is self,
and dt's date and time members are to be viewed as expressing a UTC time. The
purpose of fromutc() is to adjust the date and time members,
returning an equivalent datetime in self's local time.
Most tzinfo subclasses should be able to inherit the default fromutc() implementation without problems. It's strong enough to
handle fixed-offset time zones, and time zones accounting for both standard and daylight
time, and the latter even if the DST transition times differ in different years. An
example of a time zone the default fromutc() implementation may
not handle correctly in all cases is one where the standard offset (from UTC) depends on
the specific date and time passed, which can happen for political reasons. The default
implementations of astimezone() and fromutc()
may not produce the result you want if the result is one of the hours straddling the
moment the standard offset changes.
Skipping code for error cases, the default fromutc()
implementation acts like:
def fromutc(self, dt):
# raise ValueError error if dt.tzinfo is not self
dtoff = dt.utcoffset()
dtdst = dt.dst()
# raise ValueError if dtoff is None or dtdst is None
delta = dtoff - dtdst # this is self's standard offset
if delta:
dt += delta # convert to standard local time
dtdst = dt.dst()
# raise ValueError if dtdst is None
if dtdst:
return dt + dtdst
else:
return dt
Example tzinfo classes:
from datetime import tzinfo, timedelta, datetime
ZERO = timedelta(0)
HOUR = timedelta(hours=1)
# A UTC class.
class UTC(tzinfo):
"""UTC"""
def utcoffset(self, dt):
return ZERO
def tzname(self, dt):
return "UTC"
def dst(self, dt):
return ZERO
utc = UTC()
# A class building tzinfo objects for fixed-offset time zones.
# Note that FixedOffset(0, "UTC") is a different way to build a
# UTC tzinfo object.
class FixedOffset(tzinfo):
"""Fixed offset in minutes east from UTC."""
def __init__(self, offset, name):
self.__offset = timedelta(minutes = offset)
self.__name = name
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return ZERO
# A class capturing the platform's idea of local time.
import time as _time
STDOFFSET = timedelta(seconds = -_time.timezone)
if _time.daylight:
DSTOFFSET = timedelta(seconds = -_time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
class LocalTimezone(tzinfo):
def utcoffset(self, dt):
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
def dst(self, dt):
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
def tzname(self, dt):
return _time.tzname[self._isdst(dt)]
def _isdst(self, dt):
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, -1)
stamp = _time.mktime(tt)
tt = _time.localtime(stamp)
return tt.tm_isdst > 0
Local = LocalTimezone()
# A complete implementation of current DST rules for major US time zones.
def first_sunday_on_or_after(dt):
days_to_go = 6 - dt.weekday()
if days_to_go:
dt += timedelta(days_to_go)
return dt
# In the US, DST starts at 2am (standard time) on the first Sunday in April.
DSTSTART = datetime(1, 4, 1, 2)
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
# which is the first Sunday on or after Oct 25.
DSTEND = datetime(1, 10, 25, 1)
class USTimeZone(tzinfo):
def __init__(self, hours, reprname, stdname, dstname):
self.stdoffset = timedelta(hours=hours)
self.reprname = reprname
self.stdname = stdname
self.dstname = dstname
def __repr__(self):
return self.reprname
def tzname(self, dt):
if self.dst(dt):
return self.dstname
else:
return self.stdname
def utcoffset(self, dt):
return self.stdoffset + self.dst(dt)
def dst(self, dt):
if dt is None or dt.tzinfo is None:
# An exception may be sensible here, in one or both cases.
# It depends on how you want to treat them. The default
# fromutc() implementation (called by the default astimezone()
# implementation) passes a datetime with dt.tzinfo is self.
return ZERO
assert dt.tzinfo is self
# Find first Sunday in April & the last in October.
start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
# Can't compare naive to aware objects, so strip the timezone from
# dt first.
if start <= dt.replace(tzinfo=None) < end:
return HOUR
else:
return ZERO
Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
Central = USTimeZone(-6, "Central", "CST", "CDT")
Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
Note that there are unavoidable subtleties twice per year in a tzinfo
subclass accounting for both standard and daylight time, at the DST transition points. For
concreteness, consider US Eastern (UTC -0500), where EDT begins the minute after 1:59 (EST) on
the first Sunday in April, and ends the minute after 1:59 (EDT) on the last Sunday in October:
UTC 3:MM 4:MM 5:MM 6:MM 7:MM 8:MM
EST 22:MM 23:MM 0:MM 1:MM 2:MM 3:MM
EDT 23:MM 0:MM 1:MM 2:MM 3:MM 4:MM
start 22:MM 23:MM 0:MM 1:MM 3:MM 4:MM
end 23:MM 0:MM 1:MM 1:MM 2:MM 3:MM
When DST starts (the "start" line), the local wall clock leaps from 1:59 to 3:00.
A wall time of the form 2:MM doesn't really make sense on that day, so astimezone(Eastern)
won't deliver a result with hour==2 on the day DST begins. In order for astimezone() to make this guarantee, the rzinfo.dst()
method must consider times in the "missing hour" (2:MM for Eastern) to be in
daylight time.
When DST ends (the "end" line), there's a potentially worse problem: there's an
hour that can't be spelled unambiguously in local wall time: the last hour of daylight time.
In Eastern, that's times of the form 5:MM UTC on the day daylight time ends. The local wall
clock leaps from 1:59 (daylight time) back to 1:00 (standard time) again. Local times of the
form 1:MM are ambiguous. astimezone() mimics the local clock's
behavior by mapping two adjacent UTC hours into the same local hour then. In the Eastern
example, UTC times of the form 5:MM and 6:MM both map to 1:MM when converted to Eastern. In
order for astimezone() to make this guarantee, the tzinfo.dst()
method must consider times in the "repeated hour" to be in standard time. This is
easily arranged, as in the example, by expressing DST switch times in the time zone's standard
local time.
Applications that can't bear such ambiguities should avoid using hybrid tzinfo
subclasses; there are no ambiguities when using UTC, or any other fixed-offset tzinfo subclass (such as a class representing only EST (fixed offset -5
hours), or only EDT (fixed offset -4 hours)).
|