https://github.com/python/cpython/commit/1a2e00c97acfe9f797228b836e2345f630d07b8e
commit: 1a2e00c97acfe9f797228b836e2345f630d07b8e
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2025-09-23T21:31:42+03:00
summary:

gh-67795: Accept any real numbers as timestamp and timeout (GH-139224)

Functions that take timestamp or timeout arguments now accept any
real numbers (such as Decimal and Fraction), not only integers or floats,
although this does not improve precision.

files:
A Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst
M Doc/library/datetime.rst
M Doc/library/os.rst
M Doc/library/select.rst
M Doc/library/signal.rst
M Doc/library/socket.rst
M Doc/library/threading.rst
M Doc/library/time.rst
M Doc/whatsnew/3.15.rst
M Lib/test/datetimetester.py
M Lib/test/test_os.py
M Lib/test/test_socket.py
M Lib/test/test_time.py
M Modules/clinic/selectmodule.c.h
M Modules/clinic/signalmodule.c.h
M Modules/posixmodule.c
M Modules/selectmodule.c
M Modules/signalmodule.c
M Modules/socketmodule.c
M Python/pytime.c

diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst
index c0ae4d66b76a7b..3892367ff06efd 100644
--- a/Doc/library/datetime.rst
+++ b/Doc/library/datetime.rst
@@ -535,6 +535,9 @@ Other constructors, all class methods:
       :c:func:`localtime` function. Raise :exc:`OSError` instead of
       :exc:`ValueError` on :c:func:`localtime` failure.
 
+   .. versionchanged:: next
+      Accepts any real number as *timestamp*, not only integer or float.
+
 
 .. classmethod:: date.fromordinal(ordinal)
 
@@ -1020,6 +1023,10 @@ Other constructors, all class methods:
    .. versionchanged:: 3.6
       :meth:`fromtimestamp` may return instances with :attr:`.fold` set to 1.
 
+   .. versionchanged:: next
+      Accepts any real number as *timestamp*, not only integer or float.
+
+
 .. classmethod:: datetime.utcfromtimestamp(timestamp)
 
    Return the UTC :class:`.datetime` corresponding to the POSIX timestamp, with
@@ -1060,6 +1067,9 @@ Other constructors, all class methods:
 
       Use :meth:`datetime.fromtimestamp` with :const:`UTC` instead.
 
+   .. versionchanged:: next
+      Accepts any real number as *timestamp*, not only integer or float.
+
 
 .. classmethod:: datetime.fromordinal(ordinal)
 
diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index dab960629b21d0..8c81e1dcd070ab 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -3618,7 +3618,8 @@ features:
      where each member is an int expressing nanoseconds.
    - If *times* is not ``None``,
      it must be a 2-tuple of the form ``(atime, mtime)``
-     where each member is an int or float expressing seconds.
+     where each member is a real number expressing seconds,
+     rounded down to nanoseconds.
    - If *times* is ``None`` and *ns* is unspecified,
      this is equivalent to specifying ``ns=(atime_ns, mtime_ns)``
      where both times are the current time.
@@ -3645,6 +3646,9 @@ features:
    .. versionchanged:: 3.6
       Accepts a :term:`path-like object`.
 
+   .. versionchanged:: next
+      Accepts any real numbers as *times*, not only integers or floats.
+
 
 .. function:: walk(top, topdown=True, onerror=None, followlinks=False)
 
@@ -4050,7 +4054,7 @@ Naturally, they are all only available on Linux.
    the timer will fire when the timer's clock
    (set by *clockid* in :func:`timerfd_create`) reaches *initial* seconds.
 
-   The timer's interval is set by the *interval* :py:class:`float`.
+   The timer's interval is set by the *interval* real number.
    If *interval* is zero, the timer only fires once, on the initial expiration.
    If *interval* is greater than zero, the timer fires every time *interval*
    seconds have elapsed since the previous expiration.
diff --git a/Doc/library/select.rst b/Doc/library/select.rst
index d2094283d54736..5b14428574c0a7 100644
--- a/Doc/library/select.rst
+++ b/Doc/library/select.rst
@@ -129,8 +129,9 @@ The module defines the following:
 
    Empty iterables are allowed, but acceptance of three empty iterables is
    platform-dependent. (It is known to work on Unix but not on Windows.)  The
-   optional *timeout* argument specifies a time-out as a floating-point number
-   in seconds.  When the *timeout* argument is omitted the function blocks 
until
+   optional *timeout* argument specifies a time-out in seconds; it may be
+   a non-integer to specify fractions of seconds.
+   When the *timeout* argument is omitted the function blocks until
    at least one file descriptor is ready.  A time-out value of zero specifies a
    poll and never blocks.
 
@@ -164,6 +165,9 @@ The module defines the following:
       :pep:`475` for the rationale), instead of raising
       :exc:`InterruptedError`.
 
+   .. versionchanged:: next
+      Accepts any real number as *timeout*, not only integer or float.
+
 
 .. data:: PIPE_BUF
 
@@ -270,6 +274,9 @@ object.
       :pep:`475` for the rationale), instead of raising
       :exc:`InterruptedError`.
 
+   .. versionchanged:: next
+      Accepts any real number as *timeout*, not only integer or float.
+
 
 .. _epoll-objects:
 
@@ -368,7 +375,9 @@ Edge and Level Trigger Polling (epoll) Objects
 
 .. method:: epoll.poll(timeout=None, maxevents=-1)
 
-   Wait for events. timeout in seconds (float)
+   Wait for events.
+   If *timeout* is given, it specifies the length of time in seconds
+   (may be non-integer) which the system will wait for events before returning.
 
    .. versionchanged:: 3.5
       The function is now retried with a recomputed timeout when interrupted by
@@ -376,6 +385,9 @@ Edge and Level Trigger Polling (epoll) Objects
       :pep:`475` for the rationale), instead of raising
       :exc:`InterruptedError`.
 
+   .. versionchanged:: next
+      Accepts any real number as *timeout*, not only integer or float.
+
 
 .. _poll-objects:
 
@@ -464,6 +476,9 @@ linearly scanned again. :c:func:`!select` is *O*\ (*highest 
file descriptor*), w
       :pep:`475` for the rationale), instead of raising
       :exc:`InterruptedError`.
 
+   .. versionchanged:: next
+      Accepts any real number as *timeout*, not only integer or float.
+
 
 .. _kqueue-objects:
 
@@ -496,7 +511,7 @@ Kqueue Objects
 
    - changelist must be an iterable of kevent objects or ``None``
    - max_events must be 0 or a positive integer
-   - timeout in seconds (floats possible); the default is ``None``,
+   - timeout in seconds (non-integers are possible); the default is ``None``,
      to wait forever
 
    .. versionchanged:: 3.5
@@ -505,6 +520,9 @@ Kqueue Objects
       :pep:`475` for the rationale), instead of raising
       :exc:`InterruptedError`.
 
+   .. versionchanged:: next
+      Accepts any real number as *timeout*, not only integer or float.
+
 
 .. _kevent-objects:
 
diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst
index b0307d3dea1170..66f31b28da2a95 100644
--- a/Doc/library/signal.rst
+++ b/Doc/library/signal.rst
@@ -478,11 +478,11 @@ The :mod:`signal` module defines the following functions:
    .. versionadded:: 3.3
 
 
-.. function:: setitimer(which, seconds, interval=0.0)
+.. function:: setitimer(which, seconds, interval=0)
 
    Sets given interval timer (one of :const:`signal.ITIMER_REAL`,
    :const:`signal.ITIMER_VIRTUAL` or :const:`signal.ITIMER_PROF`) specified
-   by *which* to fire after *seconds* (float is accepted, different from
+   by *which* to fire after *seconds* (rounded up to microseconds, different 
from
    :func:`alarm`) and after that every *interval* seconds (if *interval*
    is non-zero). The interval timer specified by *which* can be cleared by
    setting *seconds* to zero.
@@ -493,13 +493,18 @@ The :mod:`signal` module defines the following functions:
    :const:`signal.ITIMER_VIRTUAL` sends :const:`SIGVTALRM`,
    and :const:`signal.ITIMER_PROF` will deliver :const:`SIGPROF`.
 
-   The old values are returned as a tuple: (delay, interval).
+   The old values are returned as a two-tuple of floats:
+   (``delay``, ``interval``).
 
    Attempting to pass an invalid interval timer will cause an
    :exc:`ItimerError`.
 
    .. availability:: Unix.
 
+   .. versionchanged:: next
+      Accepts any real numbers as *seconds* and *interval*, not only integers
+      or floats.
+
 
 .. function:: getitimer(which)
 
@@ -676,6 +681,9 @@ The :mod:`signal` module defines the following functions:
       by a signal not in *sigset* and the signal handler does not raise an
       exception (see :pep:`475` for the rationale).
 
+   .. versionchanged:: next
+      Accepts any real number as *timeout*, not only integer or float.
+
 
 .. _signal-example:
 
diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst
index bc89a3228f0ed9..134d0962db8503 100644
--- a/Doc/library/socket.rst
+++ b/Doc/library/socket.rst
@@ -1407,11 +1407,14 @@ The :mod:`socket` module also offers various 
network-related services:
 
 .. function:: setdefaulttimeout(timeout)
 
-   Set the default timeout in seconds (float) for new socket objects.  When
+   Set the default timeout in seconds (real number) for new socket objects.  
When
    the socket module is first imported, the default is ``None``.  See
    :meth:`~socket.settimeout` for possible values and their respective
    meanings.
 
+   .. versionchanged:: next
+      Accepts any real number, not only integer or float.
+
 
 .. function:: sethostname(name)
 
@@ -2073,7 +2076,7 @@ to sockets.
 .. method:: socket.settimeout(value)
 
    Set a timeout on blocking socket operations.  The *value* argument can be a
-   nonnegative floating-point number expressing seconds, or ``None``.
+   nonnegative real number expressing seconds, or ``None``.
    If a non-zero value is given, subsequent socket operations will raise a
    :exc:`timeout` exception if the timeout period *value* has elapsed before
    the operation has completed.  If zero is given, the socket is put in
@@ -2085,6 +2088,9 @@ to sockets.
       The method no longer toggles :const:`SOCK_NONBLOCK` flag on
       :attr:`socket.type`.
 
+   .. versionchanged:: next
+      Accepts any real number, not only integer or float.
+
 
 .. method:: socket.setsockopt(level, optname, value: int)
 .. method:: socket.setsockopt(level, optname, value: buffer)
diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst
index 9a0aeb7c1287ee..c1705939fb644d 100644
--- a/Doc/library/threading.rst
+++ b/Doc/library/threading.rst
@@ -608,7 +608,7 @@ since it is impossible to detect the termination of alien 
threads.
       timeout occurs.
 
       When the *timeout* argument is present and not ``None``, it should be a
-      floating-point number specifying a timeout for the operation in seconds
+      real number specifying a timeout for the operation in seconds
       (or fractions thereof). As :meth:`~Thread.join` always returns ``None``,
       you must call :meth:`~Thread.is_alive` after :meth:`~Thread.join` to
       decide whether a timeout happened -- if the thread is still alive, the
@@ -632,6 +632,9 @@ since it is impossible to detect the termination of alien 
threads.
 
          May raise :exc:`PythonFinalizationError`.
 
+      .. versionchanged:: next
+         Accepts any real number as *timeout*, not only integer or float.
+
    .. attribute:: name
 
       A string used for identification purposes only. It has no semantics.
@@ -764,7 +767,7 @@ All methods are executed atomically.
       If a call with *blocking* set to ``True`` would block, return ``False``
       immediately; otherwise, set the lock to locked and return ``True``.
 
-      When invoked with the floating-point *timeout* argument set to a positive
+      When invoked with the *timeout* argument set to a positive
       value, block for at most the number of seconds specified by *timeout*
       and as long as the lock cannot be acquired.  A *timeout* argument of 
``-1``
       specifies an unbounded wait.  It is forbidden to specify a *timeout*
@@ -783,6 +786,9 @@ All methods are executed atomically.
       .. versionchanged:: 3.14
          Lock acquisition can now be interrupted by signals on Windows.
 
+      .. versionchanged:: next
+         Accepts any real number as *timeout*, not only integer or float.
+
 
    .. method:: release()
 
@@ -863,7 +869,7 @@ call release as many times the lock has been acquired can 
lead to deadlock.
          * If no thread owns the lock, acquire the lock and return immediately.
 
          * If another thread owns the lock, block until we are able to acquire
-           lock, or *timeout*, if set to a positive float value.
+           lock, or *timeout*, if set to a positive value.
 
          * If the same thread owns the lock, acquire the lock again, and
            return immediately. This is the difference between :class:`Lock` and
@@ -890,6 +896,9 @@ call release as many times the lock has been acquired can 
lead to deadlock.
       .. versionchanged:: 3.2
          The *timeout* parameter is new.
 
+      .. versionchanged:: next
+         Accepts any real number as *timeout*, not only integer or float.
+
 
    .. method:: release()
 
@@ -1023,7 +1032,7 @@ item to the buffer only needs to wake up one consumer 
thread.
       occurs.  Once awakened or timed out, it re-acquires the lock and returns.
 
       When the *timeout* argument is present and not ``None``, it should be a
-      floating-point number specifying a timeout for the operation in seconds
+      real number specifying a timeout for the operation in seconds
       (or fractions thereof).
 
       When the underlying lock is an :class:`RLock`, it is not released using
@@ -1150,6 +1159,9 @@ Semaphores also support the :ref:`context management 
protocol <with-locks>`.
       .. versionchanged:: 3.2
          The *timeout* parameter is new.
 
+      .. versionchanged:: next
+         Accepts any real number as *timeout*, not only integer or float.
+
    .. method:: release(n=1)
 
       Release a semaphore, incrementing the internal counter by *n*.  When it
@@ -1250,7 +1262,7 @@ method.  The :meth:`~Event.wait` method blocks until the 
flag is true.
       the internal flag did not become true within the given wait time.
 
       When the timeout argument is present and not ``None``, it should be a
-      floating-point number specifying a timeout for the operation in seconds,
+      real number specifying a timeout for the operation in seconds,
       or fractions thereof.
 
       .. versionchanged:: 3.1
diff --git a/Doc/library/time.rst b/Doc/library/time.rst
index b05c0a312dbe34..3e6f5a97b49be9 100644
--- a/Doc/library/time.rst
+++ b/Doc/library/time.rst
@@ -189,7 +189,7 @@ Functions
    .. versionadded:: 3.7
 
 
-.. function:: clock_settime(clk_id, time: float)
+.. function:: clock_settime(clk_id, time)
 
    Set the time of the specified clock *clk_id*.  Currently,
    :data:`CLOCK_REALTIME` is the only accepted value for *clk_id*.
@@ -201,6 +201,9 @@ Functions
 
    .. versionadded:: 3.3
 
+   .. versionchanged:: next
+      Accepts any real number as *time*, not only integer or float.
+
 
 .. function:: clock_settime_ns(clk_id, time: int)
 
@@ -223,6 +226,9 @@ Functions
    ``asctime(localtime(secs))``. Locale information is not used by
    :func:`ctime`.
 
+   .. versionchanged:: next
+      Accepts any real number, not only integer or float.
+
 
 .. function:: get_clock_info(name)
 
@@ -258,6 +264,9 @@ Functions
    :class:`struct_time` object. See :func:`calendar.timegm` for the inverse of 
this
    function.
 
+   .. versionchanged:: next
+      Accepts any real number, not only integer or float.
+
 
 .. function:: localtime([secs])
 
@@ -271,6 +280,9 @@ Functions
    :c:func:`gmtime` failure. It's common for this to be restricted to years
    between 1970 and 2038.
 
+   .. versionchanged:: next
+      Accepts any real number, not only integer or float.
+
 
 .. function:: mktime(t)
 
@@ -382,8 +394,7 @@ Functions
 .. function:: sleep(secs)
 
    Suspend execution of the calling thread for the given number of seconds.
-   The argument may be a floating-point number to indicate a more precise sleep
-   time.
+   The argument may be a non-integer to indicate a more precise sleep time.
 
    If the sleep is interrupted by a signal and no exception is raised by the
    signal handler, the sleep is restarted with a recomputed timeout.
@@ -428,6 +439,9 @@ Functions
    .. versionchanged:: 3.13
       Raises an auditing event.
 
+   .. versionchanged:: next
+      Accepts any real number, not only integer or float.
+
 .. index::
    single: % (percent); datetime format
 
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 295dc201ec0ae4..7b146621dddcfa 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -279,6 +279,11 @@ Other language changes
   and ends with a forward slash (``/``).
   (Contributed by Serhiy Storchaka in :gh:`134716`.)
 
+* Functions that take timestamp or timeout arguments now accept any real
+  numbers (such as :class:`~decimal.Decimal` and :class:`~fractions.Fraction`),
+  not only integers or floats, although this does not improve precision.
+  (Contributed by Serhiy Storchaka in :gh:`67795`.)
+
 
 New modules
 ===========
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 43cea44bc3d6c0..7df27206206268 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -3,6 +3,7 @@
 import contextlib
 import copy
 import decimal
+import fractions
 import io
 import itertools
 import os
@@ -2626,6 +2627,10 @@ def test_fromtimestamp(self):
         expected = time.localtime(ts)
         got = self.theclass.fromtimestamp(ts)
         self.verify_field_equality(expected, got)
+        got = self.theclass.fromtimestamp(decimal.Decimal(ts))
+        self.verify_field_equality(expected, got)
+        got = self.theclass.fromtimestamp(fractions.Fraction(ts))
+        self.verify_field_equality(expected, got)
 
     def test_fromtimestamp_keyword_arg(self):
         import time
@@ -2641,6 +2646,12 @@ def test_utcfromtimestamp(self):
         with self.assertWarns(DeprecationWarning):
             got = self.theclass.utcfromtimestamp(ts)
         self.verify_field_equality(expected, got)
+        with self.assertWarns(DeprecationWarning):
+            got = self.theclass.utcfromtimestamp(decimal.Decimal(ts))
+        self.verify_field_equality(expected, got)
+        with self.assertWarns(DeprecationWarning):
+            got = self.theclass.utcfromtimestamp(fractions.Fraction(ts))
+        self.verify_field_equality(expected, got)
 
     # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in
     # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0).
@@ -2728,6 +2739,108 @@ def utcfromtimestamp(*args, **kwargs):
             self.assertEqual(t.second, 0)
             self.assertEqual(t.microsecond, 7812)
 
+    @support.run_with_tz('MSK-03')  # Something east of Greenwich
+    def test_microsecond_rounding_decimal(self):
+        D = decimal.Decimal
+        def utcfromtimestamp(*args, **kwargs):
+            with self.assertWarns(DeprecationWarning):
+                return self.theclass.utcfromtimestamp(*args, **kwargs)
+
+        for fts in [self.theclass.fromtimestamp,
+                    utcfromtimestamp]:
+            zero = fts(D(0))
+            self.assertEqual(zero.second, 0)
+            self.assertEqual(zero.microsecond, 0)
+            one = fts(D('0.000_001'))
+            try:
+                minus_one = fts(D('-0.000_001'))
+            except OSError:
+                # localtime(-1) and gmtime(-1) is not supported on Windows
+                pass
+            else:
+                self.assertEqual(minus_one.second, 59)
+                self.assertEqual(minus_one.microsecond, 999_999)
+
+                t = fts(D('-0.000_000_1'))
+                self.assertEqual(t, zero)
+                t = fts(D('-0.000_000_9'))
+                self.assertEqual(t, minus_one)
+                t = fts(D(-1)/2**7)
+                self.assertEqual(t.second, 59)
+                self.assertEqual(t.microsecond, 992188)
+
+            t = fts(D('0.000_000_1'))
+            self.assertEqual(t, zero)
+            t = fts(D('0.000_000_5'))
+            self.assertEqual(t, zero)
+            t = fts(D('0.000_000_500_000_000_000_000_1'))
+            self.assertEqual(t, one)
+            t = fts(D('0.000_000_9'))
+            self.assertEqual(t, one)
+            t = fts(D('0.999_999_499_999_999_9'))
+            self.assertEqual(t.second, 0)
+            self.assertEqual(t.microsecond, 999_999)
+            t = fts(D('0.999_999_5'))
+            self.assertEqual(t.second, 1)
+            self.assertEqual(t.microsecond, 0)
+            t = fts(D('0.999_999_9'))
+            self.assertEqual(t.second, 1)
+            self.assertEqual(t.microsecond, 0)
+            t = fts(D(1)/2**7)
+            self.assertEqual(t.second, 0)
+            self.assertEqual(t.microsecond, 7812)
+
+    @support.run_with_tz('MSK-03')  # Something east of Greenwich
+    def test_microsecond_rounding_fraction(self):
+        F = fractions.Fraction
+        def utcfromtimestamp(*args, **kwargs):
+            with self.assertWarns(DeprecationWarning):
+                return self.theclass.utcfromtimestamp(*args, **kwargs)
+
+        for fts in [self.theclass.fromtimestamp,
+                    utcfromtimestamp]:
+            zero = fts(F(0))
+            self.assertEqual(zero.second, 0)
+            self.assertEqual(zero.microsecond, 0)
+            one = fts(F(1, 1_000_000))
+            try:
+                minus_one = fts(F(-1, 1_000_000))
+            except OSError:
+                # localtime(-1) and gmtime(-1) is not supported on Windows
+                pass
+            else:
+                self.assertEqual(minus_one.second, 59)
+                self.assertEqual(minus_one.microsecond, 999_999)
+
+                t = fts(F(-1, 10_000_000))
+                self.assertEqual(t, zero)
+                t = fts(F(-9, 10_000_000))
+                self.assertEqual(t, minus_one)
+                t = fts(F(-1, 2**7))
+                self.assertEqual(t.second, 59)
+                self.assertEqual(t.microsecond, 992188)
+
+            t = fts(F(1, 10_000_000))
+            self.assertEqual(t, zero)
+            t = fts(F(5, 10_000_000))
+            self.assertEqual(t, zero)
+            t = fts(F(5_000_000_000, 9_999_999_999_999_999))
+            self.assertEqual(t, one)
+            t = fts(F(9, 10_000_000))
+            self.assertEqual(t, one)
+            t = fts(F(9_999_995_000_000_000, 10_000_000_000_000_001))
+            self.assertEqual(t.second, 0)
+            self.assertEqual(t.microsecond, 999_999)
+            t = fts(F(9_999_995, 10_000_000))
+            self.assertEqual(t.second, 1)
+            self.assertEqual(t.microsecond, 0)
+            t = fts(F(9_999_999, 10_000_000))
+            self.assertEqual(t.second, 1)
+            self.assertEqual(t.microsecond, 0)
+            t = fts(F(1, 2**7))
+            self.assertEqual(t.second, 0)
+            self.assertEqual(t.microsecond, 7812)
+
     def test_timestamp_limits(self):
         with self.subTest("minimum UTC"):
             min_dt = self.theclass.min.replace(tzinfo=timezone.utc)
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index cd15aa10f16de8..1180e27a7a5310 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -948,6 +948,20 @@ def ns_to_sec(ns):
         # issue, os.utime() rounds towards minus infinity.
         return (ns * 1e-9) + 0.5e-9
 
+    @staticmethod
+    def ns_to_sec_decimal(ns):
+        # Convert a number of nanosecond (int) to a number of seconds 
(Decimal).
+        # Round towards infinity by adding 0.5 nanosecond to avoid rounding
+        # issue, os.utime() rounds towards minus infinity.
+        return decimal.Decimal('1e-9') * ns + decimal.Decimal('0.5e-9')
+
+    @staticmethod
+    def ns_to_sec_fraction(ns):
+        # Convert a number of nanosecond (int) to a number of seconds 
(Fraction).
+        # Round towards infinity by adding 0.5 nanosecond to avoid rounding
+        # issue, os.utime() rounds towards minus infinity.
+        return fractions.Fraction(ns, 10**9) + fractions.Fraction(1, 2*10**9)
+
     def test_utime_by_indexed(self):
         # pass times as floating-point seconds as the second indexed parameter
         def set_time(filename, ns):
@@ -968,6 +982,24 @@ def set_time(filename, ns):
             os.utime(filename, times=(atime, mtime))
         self._test_utime(set_time)
 
+    def test_utime_decimal(self):
+        # pass times as Decimal seconds
+        def set_time(filename, ns):
+            atime_ns, mtime_ns = ns
+            atime = self.ns_to_sec_decimal(atime_ns)
+            mtime = self.ns_to_sec_decimal(mtime_ns)
+            os.utime(filename, (atime, mtime))
+        self._test_utime(set_time)
+
+    def test_utime_fraction(self):
+        # pass times as Fraction seconds
+        def set_time(filename, ns):
+            atime_ns, mtime_ns = ns
+            atime = self.ns_to_sec_fraction(atime_ns)
+            mtime = self.ns_to_sec_fraction(mtime_ns)
+            os.utime(filename, (atime, mtime))
+        self._test_utime(set_time)
+
     @unittest.skipUnless(os.utime in os.supports_follow_symlinks,
                          "follow_symlinks support for utime required "
                          "for this test.")
diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py
index 0b93c36c70b705..e4e7fa20f8aab9 100644
--- a/Lib/test/test_socket.py
+++ b/Lib/test/test_socket.py
@@ -8,7 +8,9 @@
 import _thread as thread
 import array
 import contextlib
+import decimal
 import errno
+import fractions
 import gc
 import io
 import itertools
@@ -1310,10 +1312,20 @@ def testDefaultTimeout(self):
             self.assertEqual(s.gettimeout(), None)
 
         # Set the default timeout to 10, and see if it propagates
-        with socket_setdefaulttimeout(10):
-            self.assertEqual(socket.getdefaulttimeout(), 10)
+        with socket_setdefaulttimeout(10.125):
+            self.assertEqual(socket.getdefaulttimeout(), 10.125)
             with socket.socket() as sock:
-                self.assertEqual(sock.gettimeout(), 10)
+                self.assertEqual(sock.gettimeout(), 10.125)
+
+            socket.setdefaulttimeout(decimal.Decimal('11.125'))
+            self.assertEqual(socket.getdefaulttimeout(), 11.125)
+            with socket.socket() as sock:
+                self.assertEqual(sock.gettimeout(), 11.125)
+
+            socket.setdefaulttimeout(fractions.Fraction(97, 8))
+            self.assertEqual(socket.getdefaulttimeout(), 12.125)
+            with socket.socket() as sock:
+                self.assertEqual(sock.gettimeout(), 12.125)
 
             # Reset the default timeout to None, and see if it propagates
             socket.setdefaulttimeout(None)
diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py
index 5312faa50774ec..ebc25a589876a0 100644
--- a/Lib/test/test_time.py
+++ b/Lib/test/test_time.py
@@ -2,6 +2,7 @@
 from test.support import warnings_helper
 import decimal
 import enum
+import fractions
 import math
 import platform
 import sys
@@ -170,10 +171,12 @@ def test_sleep_exceptions(self):
         # Improved exception #81267
         with self.assertRaises(TypeError) as errmsg:
             time.sleep([])
-        self.assertIn("integer or float", str(errmsg.exception))
+        self.assertIn("real number", str(errmsg.exception))
 
     def test_sleep(self):
-        for value in [-0.0, 0, 0.0, 1e-100, 1e-9, 1e-6, 1, 1.2]:
+        for value in [-0.0, 0, 0.0, 1e-100, 1e-9, 1e-6, 1, 1.2,
+                      decimal.Decimal('0.02'),
+                      fractions.Fraction(1, 50)]:
             with self.subTest(value=value):
                 time.sleep(value)
 
diff --git 
a/Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst 
b/Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst
new file mode 100644
index 00000000000000..6c11c93bc1170f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-09-22-11-30-45.gh-issue-67795.fROoZt.rst
@@ -0,0 +1,3 @@
+Functions that take timestamp or timeout arguments now accept any real
+numbers (such as :class:`~decimal.Decimal` and :class:`~fractions.Fraction`),
+not only integers or floats, although this does not improve precision.
diff --git a/Modules/clinic/selectmodule.c.h b/Modules/clinic/selectmodule.c.h
index c0a5f678ad038d..26ddc6ffba75bf 100644
--- a/Modules/clinic/selectmodule.c.h
+++ b/Modules/clinic/selectmodule.c.h
@@ -26,7 +26,7 @@ PyDoc_STRVAR(select_select__doc__,
 "gotten from a fileno() method call on one of those.\n"
 "\n"
 "The optional 4th argument specifies a timeout in seconds; it may be\n"
-"a floating-point number to specify fractions of seconds.  If it is absent\n"
+"a non-integer to specify fractions of seconds.  If it is absent\n"
 "or None, the call will never time out.\n"
 "\n"
 "The return value is a tuple of three lists corresponding to the first three\n"
@@ -973,7 +973,7 @@ PyDoc_STRVAR(select_epoll_poll__doc__,
 "Wait for events on the epoll file descriptor.\n"
 "\n"
 "  timeout\n"
-"    the maximum time to wait in seconds (as float);\n"
+"    the maximum time to wait in seconds (with fractions);\n"
 "    a timeout of None or -1 makes poll wait indefinitely\n"
 "  maxevents\n"
 "    the maximum number of events returned; -1 means no limit\n"
@@ -1262,7 +1262,7 @@ PyDoc_STRVAR(select_kqueue_control__doc__,
 "    The maximum number of events that the kernel will return.\n"
 "  timeout\n"
 "    The maximum time to wait in seconds, or else None to wait forever.\n"
-"    This accepts floats for smaller timeouts, too.");
+"    This accepts non-integers for smaller timeouts, too.");
 
 #define SELECT_KQUEUE_CONTROL_METHODDEF    \
     {"control", _PyCFunction_CAST(select_kqueue_control), METH_FASTCALL, 
select_kqueue_control__doc__},
@@ -1399,4 +1399,4 @@ select_kqueue_control(PyObject *self, PyObject *const 
*args, Py_ssize_t nargs)
 #ifndef SELECT_KQUEUE_CONTROL_METHODDEF
     #define SELECT_KQUEUE_CONTROL_METHODDEF
 #endif /* !defined(SELECT_KQUEUE_CONTROL_METHODDEF) */
-/*[clinic end generated code: output=2a66dd831f22c696 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=ae54d65938513132 input=a9049054013a1b77]*/
diff --git a/Modules/clinic/signalmodule.c.h b/Modules/clinic/signalmodule.c.h
index b0cd9e2e561640..9fd24d15bf2500 100644
--- a/Modules/clinic/signalmodule.c.h
+++ b/Modules/clinic/signalmodule.c.h
@@ -600,7 +600,7 @@ PyDoc_STRVAR(signal_sigtimedwait__doc__,
 "\n"
 "Like sigwaitinfo(), but with a timeout.\n"
 "\n"
-"The timeout is specified in seconds, with floating-point numbers allowed.");
+"The timeout is specified in seconds, rounded up to nanoseconds.");
 
 #define SIGNAL_SIGTIMEDWAIT_METHODDEF    \
     {"sigtimedwait", _PyCFunction_CAST(signal_sigtimedwait), METH_FASTCALL, 
signal_sigtimedwait__doc__},
@@ -794,4 +794,4 @@ signal_pidfd_send_signal(PyObject *module, PyObject *const 
*args, Py_ssize_t nar
 #ifndef SIGNAL_PIDFD_SEND_SIGNAL_METHODDEF
     #define SIGNAL_PIDFD_SEND_SIGNAL_METHODDEF
 #endif /* !defined(SIGNAL_PIDFD_SEND_SIGNAL_METHODDEF) */
-/*[clinic end generated code: output=37ae8ebeae4178fa input=a9049054013a1b77]*/
+/*[clinic end generated code: output=42e20d118435d7fa input=a9049054013a1b77]*/
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 6da90dc95addce..b7a0110226590e 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -6678,7 +6678,7 @@ os_utime_impl(PyObject *module, path_t *path, PyObject 
*times, PyObject *ns,
         if (!PyTuple_CheckExact(times) || (PyTuple_Size(times) != 2)) {
             PyErr_SetString(PyExc_TypeError,
                          "utime: 'times' must be either"
-                         " a tuple of two ints or None");
+                         " a tuple of two numbers or None");
             return NULL;
         }
         utime.now = 0;
diff --git a/Modules/selectmodule.c b/Modules/selectmodule.c
index 107e674907cf73..19fe509ec5e32a 100644
--- a/Modules/selectmodule.c
+++ b/Modules/selectmodule.c
@@ -262,7 +262,7 @@ A file descriptor is either a socket or file object, or a 
small integer
 gotten from a fileno() method call on one of those.
 
 The optional 4th argument specifies a timeout in seconds; it may be
-a floating-point number to specify fractions of seconds.  If it is absent
+a non-integer to specify fractions of seconds.  If it is absent
 or None, the call will never time out.
 
 The return value is a tuple of three lists corresponding to the first three
@@ -277,7 +277,7 @@ descriptors can be used.
 static PyObject *
 select_select_impl(PyObject *module, PyObject *rlist, PyObject *wlist,
                    PyObject *xlist, PyObject *timeout_obj)
-/*[clinic end generated code: output=2b3cfa824f7ae4cf input=df20779a9c2f5c1e]*/
+/*[clinic end generated code: output=2b3cfa824f7ae4cf input=b0403de75cd11cc1]*/
 {
 #ifdef SELECT_USES_HEAP
     pylist *rfd2obj, *wfd2obj, *efd2obj;
@@ -305,8 +305,9 @@ select_select_impl(PyObject *module, PyObject *rlist, 
PyObject *wlist,
         if (_PyTime_FromSecondsObject(&timeout, timeout_obj,
                                       _PyTime_ROUND_TIMEOUT) < 0) {
             if (PyErr_ExceptionMatches(PyExc_TypeError)) {
-                PyErr_SetString(PyExc_TypeError,
-                                "timeout must be a float or None");
+                PyErr_Format(PyExc_TypeError,
+                             "timeout must be a real number or None, not %T",
+                             timeout_obj);
             }
             return NULL;
         }
@@ -632,8 +633,9 @@ select_poll_poll_impl(pollObject *self, PyObject 
*timeout_obj)
         if (_PyTime_FromMillisecondsObject(&timeout, timeout_obj,
                                            _PyTime_ROUND_TIMEOUT) < 0) {
             if (PyErr_ExceptionMatches(PyExc_TypeError)) {
-                PyErr_SetString(PyExc_TypeError,
-                                "timeout must be an integer or None");
+                PyErr_Format(PyExc_TypeError,
+                             "timeout must be a real number or None, not %T",
+                             timeout_obj);
             }
             return NULL;
         }
@@ -974,8 +976,9 @@ select_devpoll_poll_impl(devpollObject *self, PyObject 
*timeout_obj)
         if (_PyTime_FromMillisecondsObject(&timeout, timeout_obj,
                                            _PyTime_ROUND_TIMEOUT) < 0) {
             if (PyErr_ExceptionMatches(PyExc_TypeError)) {
-                PyErr_SetString(PyExc_TypeError,
-                                "timeout must be an integer or None");
+                PyErr_Format(PyExc_TypeError,
+                             "timeout must be a real number or None, not %T",
+                             timeout_obj);
             }
             return NULL;
         }
@@ -1565,7 +1568,7 @@ select_epoll_unregister_impl(pyEpoll_Object *self, int fd)
 select.epoll.poll
 
     timeout as timeout_obj: object = None
-      the maximum time to wait in seconds (as float);
+      the maximum time to wait in seconds (with fractions);
       a timeout of None or -1 makes poll wait indefinitely
     maxevents: int = -1
       the maximum number of events returned; -1 means no limit
@@ -1579,7 +1582,7 @@ as a list of (fd, events) 2-tuples.
 static PyObject *
 select_epoll_poll_impl(pyEpoll_Object *self, PyObject *timeout_obj,
                        int maxevents)
-/*[clinic end generated code: output=e02d121a20246c6c input=33d34a5ea430fd5b]*/
+/*[clinic end generated code: output=e02d121a20246c6c input=deafa7f04a60ebe0]*/
 {
     int nfds, i;
     PyObject *elist = NULL, *etuple = NULL;
@@ -1595,8 +1598,9 @@ select_epoll_poll_impl(pyEpoll_Object *self, PyObject 
*timeout_obj,
         if (_PyTime_FromSecondsObject(&timeout, timeout_obj,
                                       _PyTime_ROUND_TIMEOUT) < 0) {
             if (PyErr_ExceptionMatches(PyExc_TypeError)) {
-                PyErr_SetString(PyExc_TypeError,
-                                "timeout must be an integer or None");
+                PyErr_Format(PyExc_TypeError,
+                             "timeout must be a real number or None, not %T",
+                             timeout_obj);
             }
             return NULL;
         }
@@ -2291,7 +2295,7 @@ select.kqueue.control
         The maximum number of events that the kernel will return.
     timeout as otimeout: object = None
         The maximum time to wait in seconds, or else None to wait forever.
-        This accepts floats for smaller timeouts, too.
+        This accepts non-integers for smaller timeouts, too.
     /
 
 Calls the kernel kevent function.
@@ -2300,7 +2304,7 @@ Calls the kernel kevent function.
 static PyObject *
 select_kqueue_control_impl(kqueue_queue_Object *self, PyObject *changelist,
                            int maxevents, PyObject *otimeout)
-/*[clinic end generated code: output=81324ff5130db7ae input=59c4e30811209c47]*/
+/*[clinic end generated code: output=81324ff5130db7ae input=be969d2bc6f84205]*/
 {
     int gotevents = 0;
     int nchanges = 0;
@@ -2331,9 +2335,8 @@ select_kqueue_control_impl(kqueue_queue_Object *self, 
PyObject *changelist,
         if (_PyTime_FromSecondsObject(&timeout,
                                       otimeout, _PyTime_ROUND_TIMEOUT) < 0) {
             PyErr_Format(PyExc_TypeError,
-                "timeout argument must be a number "
-                "or None, got %.200s",
-                _PyType_Name(Py_TYPE(otimeout)));
+                         "timeout must be a real number or None, not %T",
+                         otimeout);
             return NULL;
         }
 
diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c
index 3c79ef1429087a..4d0e224ff757e7 100644
--- a/Modules/signalmodule.c
+++ b/Modules/signalmodule.c
@@ -1210,13 +1210,13 @@ signal.sigtimedwait
 
 Like sigwaitinfo(), but with a timeout.
 
-The timeout is specified in seconds, with floating-point numbers allowed.
+The timeout is specified in seconds, rounded up to nanoseconds.
 [clinic start generated code]*/
 
 static PyObject *
 signal_sigtimedwait_impl(PyObject *module, sigset_t sigset,
                          PyObject *timeout_obj)
-/*[clinic end generated code: output=59c8971e8ae18a64 input=955773219c1596cd]*/
+/*[clinic end generated code: output=59c8971e8ae18a64 input=f89af57d645e48e0]*/
 {
     PyTime_t timeout;
     if (_PyTime_FromSecondsObject(&timeout,
diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c
index 92e6be68192dcc..ec8b53273bc083 100644
--- a/Modules/socketmodule.c
+++ b/Modules/socketmodule.c
@@ -7182,7 +7182,7 @@ socket_setdefaulttimeout(PyObject *self, PyObject *arg)
 PyDoc_STRVAR(setdefaulttimeout_doc,
 "setdefaulttimeout(timeout)\n\
 \n\
-Set the default timeout in seconds (float) for new socket objects.\n\
+Set the default timeout in seconds (real number) for new socket objects.\n\
 A value of None indicates that new socket objects have no timeout.\n\
 When the socket module is first imported, the default is None.");
 
diff --git a/Python/pytime.c b/Python/pytime.c
index 67cf6437264490..0206467364f894 100644
--- a/Python/pytime.c
+++ b/Python/pytime.c
@@ -368,8 +368,20 @@ pytime_object_to_denominator(PyObject *obj, time_t *sec, 
long *numerator,
 {
     assert(denominator >= 1);
 
-    if (PyFloat_Check(obj)) {
+    if (PyIndex_Check(obj)) {
+        *sec = _PyLong_AsTime_t(obj);
+        *numerator = 0;
+        if (*sec == (time_t)-1 && PyErr_Occurred()) {
+            return -1;
+        }
+        return 0;
+    }
+    else {
         double d = PyFloat_AsDouble(obj);
+        if (d == -1 && PyErr_Occurred()) {
+            *numerator = 0;
+            return -1;
+        }
         if (isnan(d)) {
             *numerator = 0;
             PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a 
number)");
@@ -378,30 +390,28 @@ pytime_object_to_denominator(PyObject *obj, time_t *sec, 
long *numerator,
         return pytime_double_to_denominator(d, sec, numerator,
                                             denominator, round);
     }
-    else {
-        *sec = _PyLong_AsTime_t(obj);
-        *numerator = 0;
-        if (*sec == (time_t)-1 && PyErr_Occurred()) {
-            if (PyErr_ExceptionMatches(PyExc_TypeError)) {
-                PyErr_Format(PyExc_TypeError,
-                             "argument must be int or float, not %T", obj);
-            }
-            return -1;
-        }
-        return 0;
-    }
 }
 
 
 int
 _PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, _PyTime_round_t round)
 {
-    if (PyFloat_Check(obj)) {
+    if (PyIndex_Check(obj)) {
+        *sec = _PyLong_AsTime_t(obj);
+        if (*sec == (time_t)-1 && PyErr_Occurred()) {
+            return -1;
+        }
+        return 0;
+    }
+    else {
         double intpart;
         /* volatile avoids optimization changing how numbers are rounded */
         volatile double d;
 
         d = PyFloat_AsDouble(obj);
+        if (d == -1 && PyErr_Occurred()) {
+            return -1;
+        }
         if (isnan(d)) {
             PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a 
number)");
             return -1;
@@ -418,13 +428,6 @@ _PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, 
_PyTime_round_t round)
         *sec = (time_t)intpart;
         return 0;
     }
-    else {
-        *sec = _PyLong_AsTime_t(obj);
-        if (*sec == (time_t)-1 && PyErr_Occurred()) {
-            return -1;
-        }
-        return 0;
-    }
 }
 
 
@@ -586,39 +589,38 @@ static int
 pytime_from_object(PyTime_t *tp, PyObject *obj, _PyTime_round_t round,
                    long unit_to_ns)
 {
-    if (PyFloat_Check(obj)) {
+    if (PyIndex_Check(obj)) {
+        long long sec = PyLong_AsLongLong(obj);
+        if (sec == -1 && PyErr_Occurred()) {
+            if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
+                pytime_overflow();
+            }
+            return -1;
+        }
+
+        static_assert(sizeof(long long) <= sizeof(PyTime_t),
+                    "PyTime_t is smaller than long long");
+        PyTime_t ns = (PyTime_t)sec;
+        if (pytime_mul(&ns, unit_to_ns) < 0) {
+            pytime_overflow();
+            return -1;
+        }
+
+        *tp = ns;
+        return 0;
+    }
+    else {
         double d;
         d = PyFloat_AsDouble(obj);
+        if (d == -1 && PyErr_Occurred()) {
+            return -1;
+        }
         if (isnan(d)) {
             PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a 
number)");
             return -1;
         }
         return pytime_from_double(tp, d, round, unit_to_ns);
     }
-
-    long long sec = PyLong_AsLongLong(obj);
-    if (sec == -1 && PyErr_Occurred()) {
-        if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
-            pytime_overflow();
-        }
-        else if (PyErr_ExceptionMatches(PyExc_TypeError)) {
-            PyErr_Format(PyExc_TypeError,
-                         "'%T' object cannot be interpreted as an integer or 
float",
-                         obj);
-        }
-        return -1;
-    }
-
-    static_assert(sizeof(long long) <= sizeof(PyTime_t),
-                  "PyTime_t is smaller than long long");
-    PyTime_t ns = (PyTime_t)sec;
-    if (pytime_mul(&ns, unit_to_ns) < 0) {
-        pytime_overflow();
-        return -1;
-    }
-
-    *tp = ns;
-    return 0;
 }
 
 

_______________________________________________
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