3

I have a problem with pytz and daylight saving time. When I use the timezone Europe/Berlin, it always uses the timezone offset without DST.

Minimal example:

print(repr(pytz.timezone("Europe/Berlin")))
<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>
# Should probably be something like <DstTzInfo 'Europe/Berlin' CET+2:00:00 DST>

# Usage
from django.utils import timezone
from datetime import datetime
datetime_now = timezone.now()
print(my_time)
# Result: 00:00:00
print(datetime.combine(datetime_now, my_time, tzinfo=timezone.get_current_timezone()))
# Result: 2020-04-04 00:00:00+01:00, should be 2020-04-04 00:00:00+02:00

The minimal example for my use case would be an alarm clock. The user sets the clock to 06:00 (without thinking about timezones) and the clock should ring at 06:00 in the current timezone, i.e., 06:00+02 when it's DST and 06:00+01 otherwise for Europe/Berlin.


The implementation is a Django model using django.models.TimeField for the non-aware Time (e.g. 06:00) and I want to compare it to the current time and other TimeFields by creating a datetime object that has the current date and the time storted in the TimeField.

I am open to different suggestions about time objects (e.g using or not using django.utils.timezone) as long as I can create datetime objects that I can compare to each other and increment / decrement with timedelta objects (or some similar method).


Another minimal example (with Django only for getting the current timezone):

from django.utils import timezone
import datetime

tz = timezone.get_current_timezone()
time_now = datetime.datetime.now(tz=tz)
clock_time = datetime.time(1,2)
combined_time = datetime.combine(time_now, clock_time, tzinfo=tz)
print(tz)
print(time)
print(time_now)
print(combined_time)

results in

Europe/Berlin
01:02:00
2020-04-12 18:50:11.934754+02:00
2020-04-12 01:02:00+01:00
allo
  • 3,955
  • 8
  • 40
  • 71

1 Answers1

2

Avoid using tzinfo when building timezone-aware datetimes. See this post.

Since you're using Django, assuming TIME_ZONE = 'Europe/Berlin', we can use make_aware:

from django.utils import timezone
from datetime import datetime, time

# Get a localized datetime so that .combine gets the local date
local_now = timezone.localtime()
# localtime() is a shortcut for
# timezone.now().astimezone(timezone.get_current_timezone())

clock_time = time(1, 2)
combined_time = timezone.make_aware(datetime.combine(local_now, clock_time))
print(combined_time)

It will print

2020-04-21 01:02:00+02:00

Alternatively, use the localize function in pytz (which is used in the make_aware function definition anyways, but check the details below):

tz = timezone.get_current_timezone()  # or pytz.timezone('Europe/Berlin')
combined_time = tz.localize(datetime.combine(local_now, clock_time))
# 2020-04-21 01:02:00+02:00

If you see the Django code for timezone.py, these functions are basically pytz wrappers. In particular, check the definitions for make_aware, localtime and now.

There's one particular difference between make_aware and localize, though. Both accept the argument is_dst, but for Django's make_aware it's None by default, while it's False for pytz. This difference matters in your case if a user writes a time that doesn't exist, or happens twice, when entering DST. Here, having is_dst=None will make the function raise NonExistentTimeError or AmbiguousTimeError, respectively. Otherwise, a boolean value will cause it to guess.


Example: In Europe/Berlin this year, the clock went forward one hour on March 29, 2:00 am. Therefore, 2:30 am didn't happen in local time. Python treats this input depending on is_dst:

time_doesnt_exist = datetime(2020, 3, 29, 2, 30, 0)
print(tz.localize(time_doesnt_exist, is_dst=None))
# Raises NonExistentTimeError
print(tz.localize(time_doesnt_exist, is_dst=True))
2020-03-29 02:30:00+02:00
print(tz.localize(time_doesnt_exist, is_dst=False))
2020-03-29 02:30:00+01:00

To get the exception-raising behavior with localize:

combined_time = tz.localize(datetime.combine(local_now, clock_time), is_dst=None)

To make make_aware not raise instead:

combined_time = timezone.make_aware(
  datetime.combine(local_now, clock_time),
  is_dst=False,  # Or True...
)

A word of caution: Arithmetic on localized time

Doing arithmetic on localized datetimes requires calling normalize as a workaround for DST issues, when they arise

time_before_dst = datetime(2020, 3, 29, 1, 50, 0)
local_time_before_dst = tz.localize(time_before_dst) 
new_time = local_time_before_dst + timedelta(minutes=40)
print(new_time)
# 2020-03-29 02:30:00+01:00
# Didn't switch to DST!
print(tz.normalize(new_time))
# 2020-03-29 03:30:00+02:00
# Correctly did the switch
Kevin Languasco
  • 2,318
  • 1
  • 14
  • 20
  • Is it right to use `tz.localize(datetime.combine(my_datetime_obj, time(1, 2)))` for the `localize` variant using `localize` *after* `combine` (because `combine` does not support timezones)? Maybe you could add a full example for how to use it in the best way to your post. – allo Apr 21 '20 at 21:35
  • @allo It actually wasn't right. The localization needs to happen before the `combine` so that the clock time is inserted into the correct date. Just fixed it and added details on DST handling too – Kevin Languasco Apr 22 '20 at 01:22