https://github.com/python/cpython/commit/80040b6f6304dfd61bb42142d204f6ddea5c6cee
commit: 80040b6f6304dfd61bb42142d204f6ddea5c6cee
branch: main
author: Stan Ulbrych <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-10-20T21:26:48Z
summary:

gh-157290: Add support for `%Nf` to `datetime.str{f,p}time()` (#157704)

Co-authored-by: Karoline Pauls <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-17-20-36-01.gh-issue-157290.NfFmt.rst
M Doc/library/datetime.rst
M Doc/whatsnew/3.16.rst
M Lib/_pydatetime.py
M Lib/_strptime.py
M Lib/test/datetimetester.py
M Modules/_datetimemodule.c

diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst
index dd25200f73d474..af904a05a47f06 100644
--- a/Doc/library/datetime.rst
+++ b/Doc/library/datetime.rst
@@ -2699,6 +2699,10 @@ convenience.
 |           | number, zero-padded to 6       | 999999                 |       |
 |           | digits.                        |                        |       |
 +-----------+--------------------------------+------------------------+-------+
+|  ``%Nf``  | Fraction of a second truncated | 000, 001, ..., 999     | \(5)  |
+|           | to *N* digits, where *N* is    | (``%3f``)              |       |
+|           | from 1 to 6.                   |                        |       |
++-----------+--------------------------------+------------------------+-------+
 | ``%:z``   | UTC offset in the form         | (empty), +00:00,       | \(6)  |
 |           | ``±HH:MM[:SS[.ffffff]]``       | -04:00, +10:30,        |       |
 |           | (empty string if the object is | +06:34:15,             |       |
@@ -2721,6 +2725,9 @@ differences between platforms in handling of unsupported 
format specifiers.
    ``%D``, ``%F``, ``%n``, ``%t``, and ``%:z`` were added for
    :meth:`~.datetime.strptime`.
 
+.. versionadded:: next
+   ``%Nf`` was added.
+
 
 Technical detail
 ^^^^^^^^^^^^^^^^
@@ -2833,7 +2840,8 @@ Notes:
 
 (5)
    When used with the :meth:`~.datetime.strptime` method, the ``%f`` directive
-   accepts from one to six digits and zero pads on the right. ``%f`` is
+   accepts from one to six digits and zero pads on the right, while ``%Nf``
+   accepts exactly *N* digits. ``%f`` is
    an extension to the set of format characters in the C standard (but
    implemented separately in datetime objects, and therefore always
    available).
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index e54cf2e8bf6f9a..8e578c3c058be7 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -362,6 +362,16 @@ curses
   (Contributed by Serhiy Storchaka in :gh:`133031`.)
 
 
+datetime
+--------
+
+* Add the ``%Nf`` format code (where *N* is from 1 to 6) to
+  :meth:`datetime.datetime.strftime`, :meth:`datetime.time.strftime`,
+  :meth:`datetime.date.strftime`, :meth:`datetime.datetime.strptime` and
+  :meth:`datetime.time.strptime`.
+  (Contributed by Stan Ulbrych and Karoline Pauls in :gh:`157290`.)
+
+
 dbm.gnu
 -------
 
diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py
index 50126d039e830a..f5ac965d3cfedf 100644
--- a/Lib/_pydatetime.py
+++ b/Lib/_pydatetime.py
@@ -237,6 +237,13 @@ def _wrap_strftime(object, format, timetuple):
                         freplace = '%06d' % getattr(object,
                                                     'microsecond', 0)
                     newformat.append(freplace)
+                elif ch in '123456' and format[i:i+1] == 'f':
+                    i += 1
+                    if freplace is None:
+                        freplace = '%06d' % getattr(object,
+                                                    'microsecond', 0)
+                    precision = int(ch)
+                    newformat.append(freplace[:precision])
                 elif ch == 'z':
                     if zreplace is None:
                         if hasattr(object, "utcoffset"):
diff --git a/Lib/_strptime.py b/Lib/_strptime.py
index 3311ec88871469..a83d0a6f344cae 100644
--- a/Lib/_strptime.py
+++ b/Lib/_strptime.py
@@ -386,6 +386,8 @@ def __init__(self, locale_time=None):
             't': r'\s*',
             '%': '%',
         }
+        for n in range(1, 7):
+            mapping[f'{n}f'] = rf"(?P<f>[0-9]{{{n}}})"
         if self.locale_time.LC_alt_digits is None:
             for d in 'dmyCHIMS':
                 mapping['O' + d] = r'(?P<%s>\d\d|\d| \d)' % d
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 716c662ad453f4..09a0ef5d223d2b 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -1630,6 +1630,7 @@ def test_strftime(self):
 
         #check that this standard extension works
         t.strftime("%f")
+        self.assertEqual(t.strftime("%3f"), "000")
 
         # bpo-41260: The parameter was named "fmt" in the pure python impl.
         t.strftime(format="%f")
@@ -3189,6 +3190,14 @@ def test_more_strftime(self):
         t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
         self.assertEqual(t.strftime("%m %d %y %f %S %M %H %j"),
                                     "12 31 04 000047 33 22 06 366")
+        t = self.theclass(2004, 12, 31, 6, 22, 33, 123456)
+        self.assertEqual(t.strftime("%1f %2f %3f %4f %5f %6f"),
+                                    "1 12 123 1234 12345 123456")
+        self.assertEqual(t.strftime("%f %3f"), "123456 123")
+        self.assertEqual(t.strftime("%3f %f"), "123 123456")
+        self.assertEqual(t.strftime("%S.%3f%%3f"), "33.123%3f")
+        t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
+        self.assertEqual(t.strftime("%3f %5f"), "000 00004")
         for (s, us), z in [((33, 123), "33.000123"), ((33, 0), "33"),]:
             tz = timezone(-timedelta(hours=2, seconds=s, microseconds=us))
             t = t.replace(tzinfo=tz)
@@ -4102,6 +4111,7 @@ def test_1653736(self):
     def test_strftime(self):
         t = self.theclass(1, 2, 3, 4)
         self.assertEqual(t.strftime('%H %M %S %f'), "01 02 03 000004")
+        self.assertEqual(t.strftime('%H %M %S %3f %6f'), "01 02 03 000 000004")
         # A naive object replaces %z, %:z and %Z with empty strings.
         self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''")
 
@@ -4258,12 +4268,19 @@ def test_strptime(self):
             (self.theclass(13, 2, 47, 197000), '13:02:47.197', '%H:%M:%S.%f'),
             (self.theclass(13, 2, 47, 197000), '13:02\ud80047.197', 
'%H:%M\ud800%S.%f'),
             (self.theclass(13, 2, 47, 197000), '13\ud80002:47.197', 
'%H\ud800%M:%S.%f'),
+            (self.theclass(13, 2, 47, 197000), '13:02:47.197', '%H:%M:%S.%3f'),
+            (self.theclass(13, 2, 47, 100000), '13:02:47.1', '%H:%M:%S.%1f'),
+            (self.theclass(13, 2, 47, 197531), '13:02:47.197531', 
'%H:%M:%S.%6f'),
         ]
         for expected, string, format in inputs:
             with self.subTest(string=string, format=format):
                 got = self.theclass.strptime(string, format)
                 self.assertEqual(expected, got)
                 self.assertIs(type(got), self.theclass)
+        with self.assertRaises(ValueError):
+            self.theclass.strptime('13:02:47.19', '%H:%M:%S.%3f')
+        with self.assertRaises(ValueError):
+            self.theclass.strptime('13:02:47.1975', '%H:%M:%S.%3f')
 
     def test_strptime_tz(self):
         strptime = self.theclass.strptime
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-17-20-36-01.gh-issue-157290.NfFmt.rst 
b/Misc/NEWS.d/next/Library/2026-09-17-20-36-01.gh-issue-157290.NfFmt.rst
new file mode 100644
index 00000000000000..7a001e8784b2be
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-17-20-36-01.gh-issue-157290.NfFmt.rst
@@ -0,0 +1,4 @@
+Add the ``%Nf`` format code (where *N* is from 1 to 6) to
+:meth:`datetime.datetime.strftime`, :meth:`datetime.time.strftime`,
+:meth:`datetime.date.strftime`, :meth:`datetime.datetime.strptime` and
+:meth:`datetime.time.strptime`.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index dbd14f1d707077..511b444037ceac 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -1971,6 +1971,29 @@ wrap_strftime(PyObject *object, PyObject *format, 
PyObject *timetuple,
             }
             replacement = freplacement;
         }
+        else if (ch >= '1' && ch <= '6'
+                 && i < flen && PyUnicode_READ_CHAR(format, i) == 'f')
+        {
+            /* %Nf -> first N digits of microseconds */
+            i++;
+            if (freplacement == NULL) {
+                freplacement = make_freplacement(object);
+                if (freplacement == NULL) {
+                    goto Error;
+                }
+            }
+            if (PyUnicodeWriter_WriteSubstring(writer, format, start, end) < 
0) {
+                goto Error;
+            }
+            start = i;
+            Py_ssize_t precision = ch - '0';
+            if (PyUnicodeWriter_WriteSubstring(writer, freplacement,
+                                               0, precision) < 0)
+            {
+                goto Error;
+            }
+            continue;
+        }
         else if (normalize_century()
                  && (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C'))
         {

_______________________________________________
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