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]