Datetime Timezone conversion using pytz
This is just another post on pytz
.
There are two functions to convert datetime objects between two timezones. The second functions works for all cases. The first function fails in two cases, (3) and (4). Similar SO post did not have an issue like this. Any explanation based on the difference between localize(datetime.datetime)
and replace(tzinfo)
would be a great help.
>>> from dateutil.parser import parse
>>> import pytz
First function (buggy)
The function below uses datetime.datetime.replace(tzinfo)
.
def buggy_timezone_converter(input_dt, current_tz='UTC', target_tz='US/Eastern'):
'''input_dt is a datetime.datetime object'''
current_tz = pytz.timezone(current_tz)
target_tz = pytz.timezone(target_tz)
target_dt = input_dt.replace(tzinfo=current_tz).astimezone(target_tz)
return target_tz.normalize(target_dt)
Notice the four datetime conversion now.
(1) from UTC to EST -- OK
>>> buggy_timezone_converter(parse('2013-02-26T04:00:00'))
Out[608]: datetime.datetime(2013, 2, 25, 23, 0, tzinfo=<DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>)
(2) from UTC to EDT -- OK
>>> buggy_timezone_converter(parse('2013-05-26T04:00:00'))
Out[609]: datetime.datetime(2013, 5, 26, 0, 0, tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>)
(3) from EST to UTC -- Not OK. Time offset is 4 hours 56 minutes. It is supposed to be 5 hours
>>> buggy_timezone_converter(parse('2013-02-26T04:00:00'), target_tz='UTC', current_tz='US/Eastern')
Out[610]: datetime.datetime(2013, 2, 26, 8, 56, tzinfo=<UTC>)
(4) from EDT to UTC -- Not OK. Time offset is 4 hours 56 minutes. It is supposed to be 4 hours. Daylight saving is not considered.
>>> buggy_timezone_converter(parse('2013-05-26T04:00:00'), current_tz='US/Eastern', target_tz='UTC')
Out[611]: datetime.datetime(2013, 5, 26, 8, 56, tzinfo=<UTC>)
Second function (Works perfectly)
The function below uses pytz.timezone.localize(datetime.datetime)
. It works perfectly
def good_timezone_converter(input_dt, current_tz='UTC', target_tz='US/Eastern'):
current_tz = pytz.timezone(current_tz)
target_tz = pytz.timezone(target_tz)
target_dt = current_tz.localize(input_dt).astimezone(target_tz)
return target_tz.normalize(target_dt)
(1) from UTC to EST -- OK
>>> good_timezone_converter(parse('2013-02-26T04:00:00'))
Out[618]: datetime.datetime(2013, 2, 25, 23, 0, tzinfo=<DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>)
(2) from UTC to EDT -- OK
>>> good_timezone_converter(parse('2013-05-26T04:00:00'))
Out[619]: datetime.datetime(2013, 5, 26, 0, 0, tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>)
(3) from EST to UTC -- OK.
>>> good_timezone_converter(parse('2013-02-26T04:00:00'), current_tz='US/Eastern', target_tz='UTC')
Out[621]: datetime.datetime(2013, 2, 26, 9, 0, tzinfo=<UTC>)
(4) from EDT to UTC -- OK.
>>> good_timezone_converter(parse('2013-05-26T04:00:00'), current_tz='US/Eastern', target_tz='UTC')
Out[620]: datetime.datetime(2013, 5, 26, 8, 0, tzinfo=<UTC>)
I assume you have these questions:
- why does the first function work for UTC timezone?
- why does it fail for
'US/Eastern'
timezone (DstTzInfo
instance)? - why does the second function work for all provided examples?
The first function is incorrect because it uses d.replace(tzinfo=dsttzinfo_instance)
instead of dsttzinfo_instance.localize(d)
.
The second function is correct most of the time except during ambiguous or non-existing times e.g., during DST transitions -- you can change the behaviour by passing is_dst
parameter to .localize()
: False
(default)/True
/None
(raise an exception).
The first function works for UTC timezone because it has a fixed utc offset (zero) for any date. Other timezones such as America/New_York
may have different utc offsets at different times (Daylight saving time, war time, any time that some local politician might think is a good idea -- it can be anything -- the tz database works in most cases). To implement tzinfo.utcoffset(dt)
, tzinfo.tzname(dt)
, tzinfo.dst(dt)
methods pytz
uses a collection of DstTzInfo
instances each with a different set of (_tzname, _utcoffset, _dst)
attributes. Given dt
(date/time) and is_dst
, .localize()
method chooses an appropriate (in most cases but not always) DstTzInfo
instance from the collection. pytz.timezone('America/New_York')
returns a DstTzInfo
instance with (_tzname, _utcoffset, _dst)
attributes that correspond to some undocumented moment in time (different pytz
versions may return different values -- the current version may return tzinfo
instance that corresponds to the earliest date for which zoneinfo is available -- you don't want this value most of the time: I think the motivation behind the choice of the default value is to highlight the error (passing pytz.timezone
to datetime
constructor or .replace()
method).
To summarize: .localize()
selects appropriate utcoffset, tzname, dst values, .replace()
uses the default (inappropriate) value. UTC has only one set of utcoffset, tzname, dst therefore the default value may be used and .replace()
method works with UTC timezone. You need to pass a datetime object and is_dst
parameter to select appropriate values for other timezones such as 'America/New_York'
.
In principle, pytz
could have called localize()
method to implement utcoffset()
, tzname()
, dst()
methods even if dt.tzinfo == self
: it would make these methods O(log n) in time where n
is number of intervals with different (utcoffset, tzname, dst) values but datetime
constructor and .replace()
would work as is i.e., the explicit localize()
call would be necessary only to pass is_dst
.