https://github.com/python/cpython/commit/2b6f54368169395ec9d3b6c8e4e47eefd9f9e886
commit: 2b6f54368169395ec9d3b6c8e4e47eefd9f9e886
branch: main
author: canblmz1 <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-10-29T13:37:41Z
summary:

gh-160806: Fix precision loss in `_pydatetime.timedelta` with large float 
arguments (#160807)

Co-authored-by: Stan Ulbrych <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-28-07-34-05.gh-issue-160806.ZZRkhi.rst
M Lib/_pydatetime.py
M Lib/test/datetimetester.py

diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py
index 076183139b28ae..5bbb6d433ee2ad 100644
--- a/Lib/_pydatetime.py
+++ b/Lib/_pydatetime.py
@@ -663,14 +663,6 @@ class timedelta:
 
     def __new__(cls, days=0, seconds=0, microseconds=0,
                 milliseconds=0, minutes=0, hours=0, weeks=0):
-        # Doing this efficiently and accurately in C is going to be difficult
-        # and error-prone, due to ubiquitous overflow possibilities, and that
-        # C double doesn't have enough bits of precision to represent
-        # microseconds over 10K years faithfully.  The code here tries to make
-        # explicit where go-fast assumptions can be relied on, in order to
-        # guide the C implementation; it's way more convoluted than speed-
-        # ignoring auto-overflow-to-long idiomatic Python could be.
-
         for name, value in (
             ("days", days),
             ("seconds", seconds),
@@ -685,80 +677,35 @@ def __new__(cls, days=0, seconds=0, microseconds=0,
                     f"unsupported type for timedelta {name} component: 
{type(value).__name__}"
                 )
 
-        # Final values, all integer.
-        # s and us fit in 32-bit signed ints; d isn't bounded.
-        d = s = us = 0
-
-        # Normalize everything to days, seconds, microseconds.
-        days += weeks*7
-        seconds += minutes*60 + hours*3600
-        microseconds += milliseconds*1000
-
-        # Get rid of all fractions, and normalize s and us.
-        # Take a deep breath <wink>.
-        if isinstance(days, float):
-            dayfrac, days = _math.modf(days)
-            daysecondsfrac, daysecondswhole = _math.modf(dayfrac * (24.*3600.))
-            assert daysecondswhole == int(daysecondswhole)  # can't overflow
-            s = int(daysecondswhole)
-            assert days == int(days)
-            d = int(days)
-        else:
-            daysecondsfrac = 0.0
-            d = days
-        assert isinstance(daysecondsfrac, float)
-        assert abs(daysecondsfrac) <= 1.0
-        assert isinstance(d, int)
-        assert abs(s) <= 24 * 3600
-        # days isn't referenced again before redefinition
-
-        if isinstance(seconds, float):
-            secondsfrac, seconds = _math.modf(seconds)
-            assert seconds == int(seconds)
-            seconds = int(seconds)
-            secondsfrac += daysecondsfrac
-            assert abs(secondsfrac) <= 2.0
-        else:
-            secondsfrac = daysecondsfrac
-        # daysecondsfrac isn't referenced again
-        assert isinstance(secondsfrac, float)
-        assert abs(secondsfrac) <= 2.0
-
-        assert isinstance(seconds, int)
-        days, seconds = divmod(seconds, 24*3600)
-        d += days
-        s += int(seconds)    # can't overflow
-        assert isinstance(s, int)
-        assert abs(s) <= 2 * 24 * 3600
-        # seconds isn't referenced again before redefinition
-
-        usdouble = secondsfrac * 1e6
-        assert abs(usdouble) < 2.1e6    # exact value not critical
-        # secondsfrac isn't referenced again
-
-        if isinstance(microseconds, float):
-            microseconds = round(microseconds + usdouble)
-            seconds, microseconds = divmod(microseconds, 1000000)
-            days, seconds = divmod(seconds, 24*3600)
-            d += days
-            s += seconds
-        else:
-            microseconds = int(microseconds)
-            seconds, microseconds = divmod(microseconds, 1000000)
-            days, seconds = divmod(seconds, 24*3600)
-            d += days
-            s += seconds
-            microseconds = round(microseconds + usdouble)
-        assert isinstance(s, int)
-        assert isinstance(microseconds, int)
-        assert abs(s) <= 3 * 24 * 3600
-        assert abs(microseconds) < 3.1e6
-
-        # Just a little bit of carrying possible for microseconds and seconds.
-        seconds, us = divmod(microseconds, 1000000)
-        s += seconds
-        days, s = divmod(s, 24*3600)
-        d += days
+        # Sum the exact number of microseconds, like the C implementation:
+        # the integer part of each float is converted exactly, and only the
+        # fractional microseconds left over are added up as a float and
+        # rounded (half to even) at the end.
+        us = 0
+        leftover = 0.0
+        for value, factor in (
+            (microseconds, 1),
+            (milliseconds, 1000),
+            (seconds, 1000000),
+            (minutes, 60*1000000),
+            (hours, 3600*1000000),
+            (days, 24*3600*1000000),
+            (weeks, 7*24*3600*1000000)
+        ):
+            if isinstance(value, float):
+                frac, whole = _math.modf(value)
+                us += int(whole) * factor
+                frac, whole = _math.modf(frac * factor)
+                us += int(whole)
+                leftover += frac
+            else:
+                us += int(value * factor)
+        if leftover:
+            n, m = leftover.as_integer_ratio()
+            us = _divide_and_round(us * m + n, m)
+
+        s, us = divmod(us, 1000000)
+        d, s = divmod(s, 24*3600)
 
         assert isinstance(d, int)
         assert isinstance(s, int) and 0 <= s < 24*3600
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 53285d918c17ca..92607e381db20c 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -554,6 +554,13 @@ def test_constructor(self):
         eq(td(seconds=0.001), td(milliseconds=1))
         eq(td(milliseconds=0.001), td(microseconds=1))
 
+        # Large float args keep their sub-second part
+        eq(td(weeks=12345678.9), td(86419752, 25920, 225))
+        eq(td(hours=123456789.1), td(5144032, 75959, 999979))
+        eq(td(minutes=1234567890.1), td(857338, 70205, 999994))
+        eq(td(days=1.1, weeks=1000000.1), td(7000001, 69119, 999986))
+        eq(td(seconds=0.5, microseconds=2.0**60), td(13343998, 77407, 346976))
+
         # Check type of args to constructor
         ra(TypeError, lambda: td(weeks='1'))
         ra(TypeError, lambda: td(days='1'))
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-28-07-34-05.gh-issue-160806.ZZRkhi.rst 
b/Misc/NEWS.d/next/Library/2026-10-28-07-34-05.gh-issue-160806.ZZRkhi.rst
new file mode 100644
index 00000000000000..1eb916ce8e9843
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-28-07-34-05.gh-issue-160806.ZZRkhi.rst
@@ -0,0 +1,3 @@
+Fix the pure-Python implementation of :class:`datetime.timedelta` losing
+sub-second precision with large float arguments. It now gives the same
+results as the C implementation.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to