Python's standard library (as of 2.7.2) provides a timezone framework in datetime.tzinfo but leaves the convenience of working with real timezones as an exercise for the reader. Following is a summarized fork of Armin Ronacher's advice in http://lucumr.pocoo.org/2011/7/15/eppur-si-muove.

Basic sanity

Use a timezone module like pytz.

>>> import pytz
>>> from datetime import datetime

Capture the current time in UTC as a naive datetime (in this context, 'naive' means that timezone information is not included in the datetime object):

>>> now = datetime.utcnow()

Here, a datetime is converted to and from a UNIX a timestamp to demonstrate the equivalence of the representations. [1]

>>> from calendar import timegm
>>> ts = timegm( now.utctimetuple() )
>>> # store timestamp ts somewhere
>>> assert now.replace(microsecond=0) == datetime.utcfromtimestamp(ts)

Store naive datetimes (or derivatives) and assume they are in UTC. It may help to use field names like utc_start_time and imply UTC values instead of field names like start_time and embed different timezones and offsets (and potentially different styles of timezones and offsets) within the values. [2]

After retrieving a naive datetime, format it for local use by first asserting its UTC pedigree before reflecting the universal time from another timezone's point of view.

>>> tmp = now.replace(tzinfo=pytz.timezone('UTC'))
>>> hnl = tmp.astimezone(pytz.timezone('Pacific/Honolulu'))

The reason behind this approach boils down to: anything else is likely to bite. In general, use the *gm* and *utc* methods in the datetime, time, and calendar modules for gathering, storing, retrieving, and manipulating universal time.

Tempting pitfalls

Be aware of functions like datetime.fromtimestamp(), time.mktime(), and the hidden %s format in GNU's strftime [3] which all consult the local timezone.

The difference between utcfromtimestamp() and fromtimestamp():

>>> from time import timezone
>>> now_utc_tuple = now.utctimetuple()
>>> utc_timestamp = timegm(now_utc_tuple)
>>> dt1 = datetime.utcfromtimestamp(utc_timestamp)
>>> dt2 = datetime.fromtimestamp(utc_timestamp)
>>> tdelta = (dt1 - dt2) if (dt1 > dt2) else (dt2 - dt1)
>>> assert (timezone == 0) or (abs(tdelta.seconds) == abs(timezone))

The difference between UTC timestamps and timestamps that used time.mktime() or GNU's strftime() %s format:

>>> import time
>>> diff2 = utc_timestamp - time.mktime(now_utc_tuple)
>>> diff1 = utc_timestamp - int(now.strftime('%s'))
>>> assert (timezone == 0) or (abs(diff1) == abs(diff2) == abs(timezone))

The equivalence of fromtimestamp() and GNU's strftime() %s format:

>>> out = datetime.fromtimestamp(float(now.strftime('%s.%f')))
>>> assert out == now

astimezone(tz) vs. replace(tzinfo=tz)

This example is modified from http://www.enricozini.org/2009/debian/using-python-datetime/. We derive a Europe/Rome time from a UTC time:

>>> utc = pytz.timezone('UTC')
>>> rome = pytz.timezone('Europe/Rome')
>>> a = datetime(2008, 7, 6, 5, 4, 3, tzinfo=utc)
>>> b = a.astimezone(rome)
>>> str(a)
'2008-07-06 05:04:03+00:00'
>>> str(b)
'2008-07-06 07:04:03+02:00'

Rome uses a +1hr daylight savings and a normal UTC +1hr offset, therefore in July (month 7), the offset is two hours.

strftime() produces timestamps that differ by two hours:

>>> ts_stf = lambda t: int(t.strftime('%s'))
>>> assert ts_stf(b) - ts_stf(a) == 7200

But there is no difference in the UTC timestamps:

>>> ts_utc = lambda t: timegm(t.utctimetuple())
>>> assert ts_utc(a) == ts_utc(b)

Distinguish these time representations:

  • A - a naive datetime that may as well represent UTC
  • B - an aware datetime that asserts a UTC timezone
  • C - an aware datetime asserting the same year, month, day, hour, minute, and second values as A and B when they occurred in Rome
  • D - an aware datetime that reflects the datetime in A and B from Rome's point of view
>>> A = datetime(2008, 7, 6, 5, 4, 3)
>>> B = A.replace(tzinfo=utc)
>>> C = A.replace(tzinfo=rome)
>>> D = B.astimezone(rome)

>>> # UTC timestamp comparison
>>> assert ts_utc(A) == ts_utc(B) == ts_utc(D)
>>> assert (B - C).seconds == 3600

A and B are essentially the same datetime and zone. Although D has different time values and a different timezone than A and B, it has been adjusted relative to UTC; D reflects the same universal time as A and B from Rome's point of view. C asserts a time in Rome that just so happens to have the same time values as A and B; the same time values in UTC are at least an hour behind. The one-hour (as opposed to two-hour) difference between C and B is because daylight savings played no role in C's definition; C was not adjusted relative to anything, its time is what was assigned to it - no more or less.

>>> # GNU strftime() timestamp comparison
>>> assert ts_stf(A) == ts_stf(B) == ts_stf(C)
>>> assert (ts_stf(D) - ts_stf(B)) == 7200

Above, the GNU strftime() isn't considering timezones. A, B, and C share the same time values and produce the same timestamp. D remains two hours ahead due to lingering one-hour offsets for both UTC and daylight savings.

From the Python 2.7.3 docs:

datetime.astimezone(tz)

  Return a datetime object with new tzinfo attribute tz, adjusting the date
  and time data so the result is the same UTC time as self, but in tz‘s
  local time.

datetime.replace([year[, month[, day[, hour[, minute[, second
                 [, microsecond[, tzinfo]]]]]]]])

  Return a datetime with the same attributes, except for those attributes
  given new values by whichever keyword arguments are specified. Note that
  tzinfo=None can be specified to create a naive datetime from an aware
  datetime with no conversion of date and time data.

Validate your host's time

Because something as simple as an invalid system time has never [4] caused me hours of painful debugging, I rarely find a need for these tools that take mere seconds to run:

date
hwclock
ntpdate -q 0.pool.ntp.org
rdate -p nist.time.nosc.us

If you feel the need to use these tools, you may be interested in the helpful services at http://support.ntp.org/bin/view/Servers/NTPPoolServers and http://tf.nist.gov/tf-cgi/servers.cgi.

[1]With the exception of finding support in the calendar module, timestamps are often convenient. When they aren't, explicit translations with strftime() and strptime() are the next best approach. The %s format harped on above is a tempting, non-standard hazard. Discover more alternatives, including the elusive xml.util.iso8601 module, at http://wiki.python.org/moin/WorkingWithTime.
[2]A first_name field that stores first name values is often preferable to a name field that stores names plus a middle initial offset. :)
[3]Python's strftime() does not document a %s format option (seconds past the epoch) but it does use the system's implementation (if available) which in many cases is the GNU strftime(). See http://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html.
[4]Never.

Comments