8000 datetime subject to rounding? · Issue #89510 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

datetime subject to rounding? #89510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
dvarrazzo mannequin opened this issue Oct 2, 2021 · 5 comments
Open

datetime subject to rounding? #89510

dvarrazzo mannequin opened this issue Oct 2, 2021 · 5 comments
Labels
3.7 (EOL) end of life 3.8 (EOL) end of life 3.9 only security fixes stdlib Python modules in the Lib dir

Comments

@dvarrazzo
Copy link
Mannequin
dvarrazzo mannequin commented Oct 2, 2021
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']

@dvarrazzo
Copy link
Mannequin Author
dvarrazzo mannequin commented Oct 2, 2021

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.

@dvarrazzo dvarrazzo mannequin added 3.8 (EOL) end of life 3.9 only security fixes stdlib Python modules in the Lib dir labels Oct 2, 2021
@rhettinger
Copy link
Contributor

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

@ewjoachim
Copy link
Mannequin
ewjoachim mannequin commented Oct 3, 2021

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)

@dvarrazzo
Copy link
Mannequin Author
dvarrazzo mannequin commented Oct 28, 2021

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.

@jhohm
Copy link
Contributor
jhohm commented May 20, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.7 (EOL) end of life 3.8 (EOL) end of life 3.9 only security fixes stdlib Python modules in the Lib dir
Projects
Development

No branches or pull requests

2 participants
0