29

Ok let me first start by saying my timezone is CET/CEST. The exact moment it changes from CEST to CET (back from DST, which is GMT+2, to normal, which GMT+1, thus) is always the last Sunday of October at 3AM. In 2010 this was 31 October 3AM.

Now note the following:

>>> import datetime
>>> import pytz.reference
>>> local_tnz = pytz.reference.LocalTimezone()
>>> local_tnz.utcoffset(datetime.datetime(2010, 10, 31, 2, 12, 30))
datetime.timedelta(0, 3600)

This is wrong as explained above.

>>> local_tnz.utcoffset(datetime.datetime(2010, 10, 30, 2, 12, 30))
datetime.timedelta(0, 7200)
>>> local_tnz.utcoffset(datetime.datetime(2010, 10, 31, 2, 12, 30))
datetime.timedelta(0, 7200)

Now it is suddenly correct :/

I know there are several questions about this already, but the solution given is always "use localize", but my problem here is that the LocalTimezone does not provide that method.

In fact, I have several timestamps in milliseconds of which I need the utcoffset of the local timezone (not just mine, but of anyone using the program). One of these is 1288483950000 or Sun Oct 31 2010 02:12:30 GMT+0200 (CEST) in my timezone.

Currently I do the following to get the datetime object:

datetime.datetime.fromtimestamp(int(int(millis)/1E3)) 

and this to get the utcoffset in minutes:

-int(local_tnz.utcoffset(date).total_seconds()/60)

which, unfortunately, is wrong in many occasions :(.

Any ideas?

Note: I'm using python3.2.4, not that it should matter in this case.

EDIT:

Found the solution thanks to @JamesHolderness:

def datetimeFromMillis(millis):
    return pytz.utc.localize(datetime.datetime.utcfromtimestamp(int(int(millis)/1E3)))

def getTimezoneOffset(date):
    return -int(date.astimezone(local_tz).utcoffset().total_seconds()/60)

With local_tz equal to tzlocal.get_localzone() from the tzlocal module.

Joren Van Severen
  • 2,269
  • 2
  • 24
  • 30
  • Using Python 2.7.5 and pytz 2013b, I get different and consistent results: `datetime.timedelta(0, 3600)`. `datetime.timedelta(0, 7200)`, and `datetime.timedelta(0, 3600)` respectively. I set my timezone to CET before running the test. – martineau Jul 18 '13 at 22:19
  • 1
    Try to manually go through the lines in the utcoffset and _isdst functions on a cli, see the code here: http://bazaar.launchpad.net/~stub/pytz/devel/view/head:/src/pytz/reference.py See if you can see anything wrong. – ojs Jul 20 '13 at 14:19
  • related: [Getting computer's UTC offset in Python](http://stackoverflow.com/q/3168096/4279) – jfs Sep 18 '15 at 15:57

4 Answers4

42

According to Wikipedia, the transition to and from Summer Time occurs at 01:00 UTC.

  • At 00:12 UTC you are still in Central European Summer Time (i.e. UTC+02:00), so the local time is 02:12.

  • At 01:12 UTC you are back in the standard Central European Time (i.e. UTC+01:00), so the local time is again 02:12.

When changing from Summer Time back to standard time, the local time goes from 02:59 back to 02:00 and the hour repeats itself. So when asking for the UTC offset of 02:12 (local time), the answer could truthfully be either +01:00 or +02:00 - it depends which version of 02:12 you are talking about.

On further investigation of the pytz library, I think your problem may be that you shouldn't be using the pytz.reference implementation, which may not deal with these ambiguities very well. Quoting from the comments in the source code:

Reference tzinfo implementations from the Python docs. Used for testing against as they are only correct for the years 1987 to 2006. Do not use these for real code.

Working with ambiguous times in pytz

What you should be doing is constructing a timezone object for the appropriate timezone:

import pytz
cet = pytz.timezone('CET')

Then you can use the utcoffset method to calculate the UTC offset of a date/time in that timezone.

dt = datetime.datetime(2010, 10, 31, 2, 12, 30)
offset = cet.utcoffset(dt)

Note, that the above example will throw an AmbiguousTimeError exception, because it can't tell which of the two versions of 02:12:30 you meant. Fortunately pytz will let you specify whether you want the dst version or the standard version by setting the is_dst parameter. For example:

offset = cet.utcoffset(dt, is_dst = True)

Note that it doesn't harm to set this parameter on all calls to utcoffset, even if the time wouldn't be ambiguous. According to the documentation, it is only used during DST transition ambiguous periods to resolve that ambiguity.

How to deal with timestamps

As for dealing with timestamps, it's best you store them as UTC values for as long as possible, otherwise you potentially end up throwing away valuable information. So first convert to a UTC datetime with the datetime.utcfromtimestamp method.

dt = datetime.datetime.utcfromtimestamp(1288483950)

Then use pytz to localize the time as UTC, so the timezone is attached to the datetime object.

dt = pytz.utc.localize(dt)

Finally you can convert that UTC datetime into your local timezone, and obtain the timezone offset like this:

offset = dt.astimezone(cet).utcoffset()

Note that this set of calculations will produce the correct offsets for both 1288483950 and 1288487550, even though both timestamps are represented by 02:12:30 in the CET timezone.

Determining the local timezone

If you need to use the local timezone of your computer rather than a fixed timezone, you can't do that from pytz directly. You also can't just construct a pytz.timezone object using the timezone name from time.tzname, because the names won't always be recognised by pytz.

The solution is to use the tzlocal module - its sole purpose is to provide this missing functionality in pytz. You use it like this:

import tzlocal
local_tz = tzlocal.get_localzone()

The get_localzone() function returns a pytz.timezone object, so you should be able to use that value in all the places I've used the cet variable in the examples above.

James Holderness
  • 22,721
  • 2
  • 40
  • 52
  • I was using the latest version and the result was also wrong for 01:12 CE(S)T on the same day, so it has nothing to do with ambiguity of the date. Also it is usual the convention to assume the DST one in these transition periods, JavaScript does it this way. – Joren Van Severen Jul 21 '13 at 19:30
  • 1
    I'm curious what makes you think there is any convention regarding these transition periods. On my computer the javascript implementation differs from one browser to the next. Try typing `new Date(2010, 9, 31, 2, 12, 30, 0).getTime()` into your browser console. On Chrome I get 1288487550000 (i.e. 01:12 UTC). On Firefox I get 1288483950000 (i.e. 00:12 UTC). – James Holderness Jul 21 '13 at 20:46
  • Try with new Date(1288483950000), gives the "Sun Oct 31 2010 02:12:30 GMT+0200" on Firefox and Chrome for me. – Joren Van Severen Jul 21 '13 at 21:02
  • 1
    I'm beginning to understand the problem, there is no ambiguity when using timestamps, hence why the timezone and exact date of new Date(1288483950000) can be correctly determined. So i'm in need of a way to calculate timezoneoffset from a timestamp in python... – Joren Van Severen Jul 21 '13 at 21:13
  • Nevertheless, Python/pytz is wrong on the day of the transition. It thinks it changed on 30th October, not 31st. – Joren Van Severen Jul 21 '13 at 21:14
  • @JorenVanSeveren It seems you shouldn't be using the pytz.reference implementation - have updated my answer with more info. – James Holderness Jul 21 '13 at 21:32
  • The only problem with that is that CET is my timezone, but that's not gonna be true for everyone, so is there any way to create a pytz timezone object from the reference local timezone? – Joren Van Severen Jul 22 '13 at 07:17
  • Apparently time.tzname[False] returns 'CET', although not sure if it will work with all timezones, I'll use that for the moment. I'm at work atm, but I''ll test it out tonight and accept your answer + bounty if it works :). – Joren Van Severen Jul 22 '13 at 07:21
  • 1
    @JorenVanSeveren I'm afraid you can't rely on time.tzname to work in all cases. I've updated my answer with a solution for getting the local timezone. – James Holderness Jul 22 '13 at 08:35
  • Ok, got it to work, had some additional problems dealing with the current time, but all has been resolved (using the same utc-method you described). Thanks a lot :). – Joren Van Severen Jul 22 '13 at 20:49
  • Unfortunately, the part in this answer about utcfromtimestamp and then localising it is wrong. No room to show it here (I tried), but in the calculations the local timezone is dragged in somewhere along the line. – Ytsen de Boer Nov 23 '21 at 14:32
14

Given a timestamp in milliseconds you can get the utc offset for the local timezone using only stdlib:

#!/usr/bin/env python
from datetime import datetime

millis = 1288483950000
ts = millis * 1e-3
# local time == (utc time + utc offset)
utc_offset = datetime.fromtimestamp(ts) - datetime.utcfromtimestamp(ts)

If we ignore time around leap seconds then there is no ambiguity or non-existent times.

It supports DST and changes of the utc offset for other reasons if OS maintains a historical timezone db e.g., it should work on Ubuntu for any past/present date but might break on Windows for past dates that used different utc offset.

Here's the same using tzlocal module that should work on *nix and Win32 systems:

#!/usr/bin/env python
from datetime import datetime
from tzlocal import get_localzone # pip install tzlocal

millis = 1288483950000
ts = millis * 1e-3
local_dt = datetime.fromtimestamp(ts, get_localzone())
utc_offset = local_dt.utcoffset()

See How to convert a python utc datetime to a local datetime using only python standard library?

To get the utc offset in minutes (Python 3.2+):

from datetime import timedelta

minutes = utc_offset / timedelta(minutes=1)

Don't use pytz.reference.LocalTimezone(), it is only for tests.

Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
1
import pytz, datetime
tz = timezone('CET')
tz.utcoffset(datetime.datetime.now()).total_seconds()

7200.0

Tms91
  • 3,456
  • 6
  • 40
  • 74
AJ.
  • 310
  • 3
  • 11
-1

other_datetime = "YYYY-mm-ddTHH:MM:SSother_offset"

T may be a space or the char 'T' itself.

other_offset is usually expressed as "+HHMM" or "-HHMM"

Then you simply do this:

  1. get my own timezone offset := my_offset
  2. my_datetime = other_datetime + minutes( my_offset - other_offset)

Rembember my_offset and other_offset can be positive or negative.

So you could end up doing something like this, for example:

my_datetime = other_datetime + minutes( (-7:00) - (-8:00) ) = other_datetime + minutes(+1:00) = other_datetime + 60 minutes

etc

Note the double negative.

Klops
  • 951
  • 6
  • 18