A Random Python Timezone Bug
So this one is another interesting Python Timezone thing. It feels like every time I do something with timezones in Python, I think I’ve got them figured out and I clearly don’t. Theres always another thing. This one was interesting though and took a while to figure out. The issue turned out to be not as straight-forward as I thought it would have been.
Imagine something like this:
from datetime import datetime, timezone import pytz = "US/Eastern" TIMEZONE = pytz.timezone(TIMEZONE) tz def utc_now(): return datetime.now(timezone.utc) def tz_now(): return convert_to_local(get_utc_now()) def convert_to_local(any_time): return any_time.astimezone(tz) def convert_to_utc(any_time): return any_time.astimezone(pytz.utc)
With me so far? Simple stuff, and this is all the correct way to do timezones in Python: No naive timestamps, no replace, use UTC for computation and localtime for display, etc. Good, let’s keep going:
# Get the first of the month, in the local time: = tz_now().replace(day=1, hour=0, minute=0, local_first_of_month =0, microsecond=0) second# datetime.datetime(2020, 3, 1, 0, 0, # tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>) # Convert it to utc: = convert_to_utc(local_first_of_month) utc_first_of_month # datetime.datetime(2020, 3, 1, 4, 0, # tzinfo=datetime.timezone.utc) # Convert it back and ??? convert_to_local(utc_first_of_month)# datetime.datetime(2020, 2, 29, 23, 0, # tzinfo=<DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>)
Whoa, wait a minute, what happened there? Why aren’t they the same?
Should going between UTC and your local timezone be idempotent? Well,
you’d be correct, except that things go a little haywire when you call
replace() will probably work just
fine in the vast majority of cases, except when you cross a
daylight savings-time boundary. You’ll notice here that the two
timestamps have the same timezone name, but differ in Daylight-ness:
The reason it isn’t “idempotent” here is because that doesn’t really
make any sense, since you’re crossing a datetime boundary: Though
timezones are technically the same, the hour delta between UTC changes
because of daylight savings time. This only happens when you do a
replace(), because you’re just modifying values on an
existing timezone. Fascinating, but REAL annoying. Heres the correct
implementation: instead of doing a replace, build the datetime from the
# Get the first of the month, in the local time: = datetime(year=this.year, month=this.month, day=1, local_first_of_month =0, minute=0, second=0, microsecond=0) hour= tz.localize(local_first_of_month)local_first_of_month
This works as you’d expect, and works regardless of when you run the
code since you’re not doing a replace on
Good luck out there, soldier.