cpython icon indicating copy to clipboard operation
cpython copied to clipboard

datetime subject to rounding?

Open 200a09ed-fa9e-4f1a-b485-a130c25efc9b opened this issue 4 years ago • 5 comments

BPO 45347
Nosy @rhettinger, @dvarrazzo, @asqui, @ewjoachim

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2021-10-02.17:01:42.584>
labels = ['3.8', '3.7', 'library', '3.9']
title = 'datetime subject to rounding?'
updated_at = <Date 2021-10-28.22:37:10.910>
user = 'https://github.com/dvarrazzo'

bugs.python.org fields:

activity = <Date 2021-10-28.22:37:10.910>
actor = 'piro'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Library (Lib)']
creation = <Date 2021-10-02.17:01:42.584>
creator = 'piro'
dependencies = []
files = []
hgrepos = []
issue_num = 45347
keywords = []
message_count = 4.0
messages = ['403059', '403062', '403078', '405284']
nosy_count = 4.0
nosy_names = ['rhettinger', 'piro', 'dfortunov', 'ewjoachim']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = None
url = 'https://bugs.python.org/issue45347'
versions = ['Python 3.7', 'Python 3.8', 'Python 3.9']

I found two datetimes at difference timezone whose difference is 0 but which don't compare equal.

Python 3.9.5 (default, May 12 2021, 15:26:36) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime as dt
>>> from zoneinfo import ZoneInfo
>>> for i in range(3):
...     ref = dt.datetime(5327 + i, 10, 31, tzinfo=dt.timezone.utc)
...     print(ref.astimezone(ZoneInfo(key='Europe/Rome')) == ref.astimezone(dt.timezone(dt.timedelta(seconds=3600))))
... 
True
False
True
>>> for i in range(3):
...     ref = dt.datetime(5327 + i, 10, 31, tzinfo=dt.timezone.utc)
...     print(ref.astimezone(ZoneInfo(key='Europe/Rome')) - ref.astimezone(dt.timezone(dt.timedelta(seconds=3600))))
... 
0:00:00
0:00:00
0:00:00

Is this a float rounding problem? If so I think it should be documented that datetimes bewhave like floats instead of like Decimal, although they have finite precision.

Related: https://bugs.python.org/issue44831

rhettinger avatar Oct 02 '21 19:10 rhettinger

It may or it may not be obvious to some, but in year 5328, October 31st is the last Sunday of October, which in Rome, as in the rest of EU, according to the 202X rules, means it’s the day we shift from summer time (in Rome UTC+2) to standard time (in Rome UTC+1). The shift supposedly happens at 3AM where it’s 2AM, so not at midnight, but the proximity to a daylight shift moment raises some eyebrows. This could explain why it doesn’t happen on the year before or after. (I’m curious if it happens on year 5334 which has the same setup, but I cannot check at the moment)

Considering that I have found another pair of dates failing equality, and they are too on the last Sunday of October, the hypothesis of rounding in timezone code starts to look likely....

Python 3.7.9 (default, Jan 12 2021, 17:26:22) [GCC 8.3.0] on linux

>>> import datetime, backports.zoneinfo
>>> d1 = datetime.datetime(2255, 10, 28, 7, 31, 21, 393428, tzinfo=datetime.timezone(datetime.timedelta(seconds=27060)))
>>> d2 = datetime.datetime(2255, 10, 28, 2, 0, 21, 393428, tzinfo=backports.zoneinfo.ZoneInfo(key='Europe/Rome'))
>>> d1 - d2
datetime.timedelta(0)
>>> d1 == d2
False

Added Python 3.7 to the affected list.

This is still reproducible at head (3.15 alpha), with both the C and Python versions of datetime and zoneinfo:

>>> import datetime, zoneinfo
>>> d1 = datetime.datetime(2255, 10, 28, 7, 31, 21, 393428, tzinfo=datetime.timezone(datetime.timedelta(seconds=27060)))
>>> d2 = datetime.datetime(2255, 10, 28, 2, 0, 21, 393428, tzinfo=zoneinfo.ZoneInfo(key='Europe/Rome'))
>>> d1 - d2
datetime.timedelta(0)
>>> d1 == d2
False
>>> import sys
>>> sys.modules['_datetime'] = None
>>> sys.modules['_zoneinfo'] = None
>>> import datetime, zoneinfo
>>> d1 = datetime.datetime(2255, 10, 28, 7, 31, 21, 393428, tzinfo=datetime.timezone(datetime.timedelta(seconds=27060)))
>>> d2 = datetime.datetime(2255, 10, 28, 2, 0, 21, 393428, tzinfo=zoneinfo.ZoneInfo(key='Europe/Rome'))
>>> d1 - d2
datetime.timedelta(0)
>>> d1 == d2
False

Looking at the Python implementation of __eq__:

            return self._cmp(other, allow_mixed=True) == 0

And the Python implementation of __cmp__ for when tzinfo are not the same:

            myoff = self.utcoffset()
            otoff = other.utcoffset()
            # Assume that allow_mixed means that we are called from __eq__
            if allow_mixed:
                if myoff != self.replace(fold=not self.fold).utcoffset():
                    return 2
                if otoff != other.replace(fold=not other.fold).utcoffset():
                    return 2

So we can dig in further in our example:

>>> d1._cmp(d2, allow_mixed=True)
2
>>> d1.utcoffset() == d1.replace(fold=not d1.fold).utcoffset()
True
>>> d2.utcoffset() == d2.replace(fold=not d2.fold).utcoffset()
False
>>> d2.utcoffset()
datetime.timedelta(seconds=7200)
>>> d2.replace(fold=not d2.fold).utcoffset()
datetime.timedelta(seconds=3600)

Okay, clearly this is a deliberate choice with differing timezone info when one of the datetimes is in a repeated interval (i.e. utcoffset() changes based on fold). And looking at the documentation (updated in gh-114728 to reflect behavior since 2006) this is explicitly intended (emphasis mine):

datetime objects are equal if they represent the same date and time, taking into account the time zone.

Naive and aware datetime objects are never equal.

If both comparands are aware, and have the same tzinfo attribute, the tzinfo and fold attributes are ignored and the base datetimes are compared. If both comparands are aware and have different tzinfo attributes, the comparison acts as comparands were first converted to UTC datetimes except that the implementation never overflows. datetime instances in a repeated interval are never equal to datetime instances in other time zone.

jhohm avatar May 20 '25 14:05 jhohm