10000 datetime.timestamp() doesn't have enough precision to represent datetime.max · Issue #91012 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

datetime.timestamp() doesn't have enough precision to represent datetime.max #91012

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
jorisgeysens mannequin opened this issue Feb 25, 2022 · 21 comments
Open

datetime.timestamp() doesn't have enough precision to represent datetime.max #91012

jorisgeysens mannequin opened this issue Feb 25, 2022 · 21 comments
Labels
docs Documentation in the Doc dir extension-modules C modules in the Modules dir stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@jorisgeysens
Copy link
Mannequin
jorisgeysens mannequin commented Feb 25, 2022
BPO 46856
Nosy @ericvsmith

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 2022-02-25.16:10:01.560>
labels = ['type-bug', 'library', '3.9']
title = 'datetime.max conversion'
updated_at = <Date 2022-03-01.15:47:18.405>
user = 'https://bugs.python.org/jorisgeysens'

bugs.python.org fields:

activity = <Date 2022-03-01.15:47:18.405>
actor = 'joris.geysens'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Library (Lib)']
creation = <Date 2022-02-25.16:10:01.560>
creator = 'joris.geysens'
dependencies = []
files = []
hgrepos = []
issue_num = 46856
keywords = []
message_count = 6.0
messages = ['414013', '414023', '414032', '414043', '414044', '414275']
nosy_count = 2.0
nosy_names = ['eric.smith', 'joris.geysens']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue46856'
versions = ['Python 3.9']

@jorisgeysens
Copy link
Mannequin Author
jorisgeysens mannequin commented Feb 25, 2022

Reading the documentation, I don't understand how this is not possible :

# get the max utc timestamp
ts = datetime.max.replace(tzinfo=timezone.utc).timestamp()

# similarly 
ts2 = datetime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=timezone.utc).timestamp()

# timestamp value 253402300800 seems correct
# converting back to timestamp is impossible, these all fail :

dt = datetime.fromtimestamp(ts, tz=timezone.utc)
dt = datetime.utcfromtimestamp(ts)

It should be possible to get a datetime back from the initially converted timestamp, no?

@jorisgeysens jorisgeysens mannequin added 3.9 only security fixes type-bug An unexpected behavior, bug, or error labels Feb 25, 2022
@ericvsmith
Copy link
Member

Please show us how they fail.

@jorisgeysens
Copy link
Mannequin Author
jorisgeysens mannequin commented Feb 25, 2022

a ValueError is raised :

ValueError: year 10000 is out of range

on

dt = datetime.fromtimestamp(ts, tz=timezone.utc)

or

dt = datetime.utcfromtimestamp(ts)

@jorisgeysens
Copy link
Mannequin Author
jorisgeysens mannequin commented Feb 25, 2022

I see this in the python source code being tested (datetimetester.py), so I guess it is a rounding problem :

# maximum timestamp: set seconds to zero to avoid rounding issues
        max_dt = self.theclass.max.replace(tzinfo=timezone.utc,
                                           second=0, microsecond=0)
        max_ts = max_dt.timestamp()
        # date 9999-12-31 23:59:00+00:00: timestamp 253402300740
        self.assertEqual(self.theclass.fromtimestamp(max_ts, tz=timezone.utc),
                         max_dt)

@jorisgeysens jorisgeysens mannequin added stdlib Python modules in the Lib dir labels Feb 25, 2022
@ericvsmith
Copy link
Member

Probably so. You could step through the code to make sure that's what's going on.

@jorisgeysens
Copy link
Mannequin Author
jorisgeysens mannequin commented Mar 1, 2022

I looked at this a bit more in detail.
What happens it the following, starting in the datetime fromtimestamp fragment :

converter = _time.gmtime if utc else _time.localtime
y, m, d, hh, mm, ss, weekday, jday, dst = converter(t)

That will call the system gmtime_r(), which indeed returns Sat Jan 1 00:00:00 10000

I have not looked at the cpp implementation for that method and I am not sure if this is something that can be improved.

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
@psarka
Copy link
psarka commented Apr 15, 2022

The min range of the datetime is also affected:

>>> import datetime as dt
>>> dt.datetime.min.timestamp()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: year 0 is out of range

@pganssle
Copy link
Member

I have noticed that the behavior of datetime.min.timestamp, datetime.max.timestamp and datetime.fromtimestamp that would result in values close to the minimum and maximum are failing in certain locales basically because of bugs in our code. I'm going to call this the issue for tracking that.

In reality, this does not matter, because timestamps that far away from the current date are essentially meaningless, but we should probably have well-defined limits so that people can reason well enough about their code. I propose that on all platforms that support it (e.g. not Windows):

  1. datetime.fromtimestamp() and datetime.utcfromtimestamp() should always work if the timestamp represents a datetime that can be represented by the datetime.class.
  2. datetime.timestamp() should never error out because some intermediate calculation hits some datetime outside the allowed range.

@donBarbos
Copy link
Contributor
donBarbos commented Feb 26, 2025

I think it's important to note that the problem is not with the fromtimestamp method, the point is that datetime.timestamp returns a rounded timestamp because 253402300800 is exactly 10000 years, but we asked timestamp for 9999 year 12, 31, .... it's should be 253402300800 - 1 = 253402300799

the thing is that in the datetime.max object have 999999 microseconds and this is rounded to 1 second, which is added to the 59 seconds + 1

Sorry if it's obvious

@donBarbos
Copy link
Contributor
donBarbos commented Feb 26, 2025

we have a method timedelta.total_seconds (we using this method in datetime.timestamp like this return (self - _EPOCH).total_seconds()):

    def total_seconds(self):
        """Total seconds in the duration."""
        return ((self.days * 86400 + self.seconds) * 10**6 + self.microseconds) / 10**6

it returns the rounded number of seconds, the question arose: should we do this? (maybe we should open new issue)

in my opinion rounding up is the wrong approach in the module responsible for date/time

another example:

>>> days * 86400 + seconds
253402300799 # ok
>>> ((days * 86400 + seconds) * 10**6 + microseconds) / 10**6
253402300800.0 # not ok
>>> days, seconds, microseconds = Decimal(days), Decimal(seconds), Decimal(microseconds)
>>> ((days * 86400 + seconds) * 10**6 + microseconds) / 10**6
Decimal('253402300799.999999')

@StanFromIreland
Copy link
Contributor

Duplicate of: #89510, #88994 ?

@picnixz picnixz added extension-modules C modules in the Modules dir and removed 3.9 only security fixes labels Mar 9, 2025
@StanFromIreland
Copy link
Contributor
StanFromIreland commented Mar 15, 2025

We are hitting the limit of Python's float, causing rounding.

>>> float("-30610222479.99999")
-30610222479.99999
>>> float("-30610222479.999999")
-30610222480.0

What if we dropped a digit off of max, that would suffice? We currently have a 0 <= microsecond < 1000000 bound on microseconds what if we make it 0-99999?

@picnixz

This comment has been minimized.

@StanFromIreland

This comment has been minimized.

@ericvsmith
Copy link
Member

What if we dropped a digit off of max, that would suffice? We currently have a 0 <= microsecond < 1000000 bound on microseconds what if we make it 0-99999?

I don't think we want to do that: it unnecessarily complicates the code and might have unintended effects. I don't think there's a practical problem here that needs addressing.

@StanFromIreland
Copy link
Contributor

The problem still exists, if a user specifies too many microseconds that float cannot handle -- it rounds into the next day.

@picnixz
Copy link
Member
picnixz commented Mar 16, 2025

The problem still exists, if a user specifies too many microseconds that float cannot handle -- it rounds into the next day.

If too many microseconds are specified, and rounding is not wanted, that's an API misuse IMO. The same could be said for floats themselves. If you specify too many digits, then the result is rounded.

What if we dropped a digit off of max, that would suffice? We currently have a 0 <= microsecond < 1000000 bound on microseconds what if we make it 0-99999?

Note that this would exclude float("-30610222479.999998") which would still be valid as it's -30610222479.99999618530273437500. So, if we were to change the limit, it should be < 999999 instead of 1000000. But again, I don't think we need to overcomplicate the code for this specific event (I don't know when, in practice, we would have this issue and how frequent it would appear).

@StanFromIreland
Copy link
Contributor

Do we just want to note this in the docs and close?

@ericvsmith
Copy link
Member

I personally don't think it's worth a note in the docs. It's how floating point works. How many places would we make this notation?

@StanFromIreland
Copy link
Contributor

Maybe just once in the notes at the start "due to the limitations of double and Python's float microseconds may be rounded" though I still think there should be a better approach stopping the rounding.

@encukou encukou changed the title datetime.max conversion datetime.timestamp() doesn't have enough precision to represent datetime.max Mar 24, 2025
@encukou encukou marked this as a duplicate of #90683 Mar 24, 2025
@encukou
Copy link
Member
encukou commented Mar 24, 2025

In the time module, the solution was to add time_ns, and other *_ns variants for all functions that dealt with float timestamps.
But time deals with current times, and the limits of the internal underlying C type are not enough for datetime, so any *_ns functions would need to be implemented differently. I don't think that's worth it.

Ultimately, datetime.timestamp() returns float, and float is subject to rounding. If you don't want a lossy export, don't use timestamp().

@encukou encukou added the docs Documentation in the Doc dir label Mar 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Documentation in the Doc dir extension-modules C modules in the Modules dir stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
Status: Todo
Development

No branches or pull requests

7 participants
0