Datetime Timezone conversion using pytz
datetime, python, python-2.7, pytz, timezone
Solution
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`.
Problem
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>) ```