From f884dc6f70fde3550a175ff7ddce79ccc38b3ec6 Mon Sep 17 00:00:00 2001 From: "Jason Yalim, PhD" <4813268+jyalim@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:00:39 -0700 Subject: [PATCH] gh-140715: Add %t and %n format codes support to strptime() (GH-144896) Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Doc/library/datetime.rst | 14 +++++++++----- Lib/_strptime.py | 5 ++++- Lib/test/datetimetester.py | 14 ++++++++++++++ Lib/test/test_strptime.py | 17 +++++++++++++++++ Lib/test/test_time.py | 2 +- ...26-02-17-03-43-07.gh-issue-140715.twmcM_.rst | 1 + 6 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-17-03-43-07.gh-issue-140715.twmcM_.rst diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 73217136f144..8993049a720b 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2611,8 +2611,10 @@ requires, and these work on all supported platforms. | ``%M`` | Minute as a zero-padded | 00, 01, ..., 59 | \(9) | | | decimal number. | | | +-----------+--------------------------------+------------------------+-------+ -| ``%n`` | The newline character | ``\n`` | \(0) | -| | (``'\n'``). | | | +| ``%n`` | The newline character | ``\n`` | | +| | (``'\n'``). For | | | +| | :meth:`!strptime`, zero or | | | +| | more whitespace. | | | +-----------+--------------------------------+------------------------+-------+ | ``%p`` | Locale's equivalent of either || AM, PM (en_US); | \(1), | | | AM or PM. || am, pm (de_DE) | \(3) | @@ -2625,8 +2627,9 @@ requires, and these work on all supported platforms. | ``%S`` | Second as a zero-padded | 00, 01, ..., 59 | \(4), | | | decimal number. | | \(9) | +-----------+--------------------------------+------------------------+-------+ -| ``%t`` | The tab character | ``\t`` | \(0) | -| | (``'\t'``). | | | +| ``%t`` | The tab character (``'\t'``). | ``\t`` | | +| | For :meth:`!strptime`, | | | +| | zero or more whitespace. | | | +-----------+--------------------------------+------------------------+-------+ | ``%T`` | ISO 8601 time format, | 10:01:59 | | | | equivalent to ``%H:%M:%S``. | | | @@ -2717,7 +2720,8 @@ differences between platforms in handling of unsupported format specifiers. ``%:z`` was added for :meth:`~.datetime.strftime`. .. versionadded:: 3.15 - ``%:z``, ``%F``, and ``%D`` were added for :meth:`~.datetime.strptime`. + ``%D``, ``%F``, ``%n``, ``%t``, and ``%:z`` were added for + :meth:`~.datetime.strptime`. Technical detail diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 0d81ff6765e1..3367ac485a59 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -382,7 +382,10 @@ class TimeRE(dict): 'Z': self.__seqToRE((tz for tz_names in self.locale_time.timezone for tz in tz_names), 'Z'), - '%': '%'} + 'n': r'\s*', + 't': r'\s*', + '%': '%', + } 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 97eec618932a..e264433ca590 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2207,6 +2207,20 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): self.theclass.strptime(test_date, "%m/%d/%y") ) + def test_strptime_n_and_t_format(self): + format_directives = ('%n', '%t', '%n%t', '%t%n') + whitespaces = ('', ' ', '\t', '\r', '\v', '\n', '\f') + for fd in format_directives: + for ws in (*whitespaces, ''.join(whitespaces)): + with self.subTest(format_directive=fd, whitespace=ws): + self.assertEqual( + self.theclass.strptime( + f"2026{ws}02{ws}03", + f"%Y{fd}%m{fd}%d", + ), + self.theclass(2026, 2, 3), + ) + ############################################################################# # datetime tests diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index fd8525feb88d..dfc8ef6d2c5b 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -670,6 +670,23 @@ class StrptimeTests(unittest.TestCase): time.strptime(test_date, "%m/%d/%y") ) + def test_strptime_n_and_t_format(self): + format_directives = ('%n', '%t', '%n%t', '%t%n') + whitespaces = ('', ' ', '\t', '\r', '\v', '\n', '\f') + for fd in format_directives: + for ws in (*whitespaces, ''.join(whitespaces)): + with self.subTest(format_directive=fd, whitespace=ws): + self.assertEqual( + time.strptime( + f"2026{ws}02{ws}03", + f"%Y{fd}%m{fd}%d", + ), + time.strptime( + f'2026-02-03', + "%Y-%m-%d", + ), + ) + class Strptime12AMPMTests(unittest.TestCase): """Test a _strptime regression in '%I %p' at 12 noon (12 PM)""" diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index da5fd16b8b62..be8f6b057654 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -359,7 +359,7 @@ class TimeTestCase(unittest.TestCase): # raising an exception. tt = time.gmtime(self.t) for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'D', 'F', 'H', 'I', - 'j', 'm', 'M', 'p', 'S', 'T', + 'j', 'm', 'M', 'n', 'p', 'S', 't', 'T', 'U', 'w', 'W', 'x', 'X', 'y', 'Y', 'Z', '%'): format = '%' + directive if directive == 'd': diff --git a/Misc/NEWS.d/next/Library/2026-02-17-03-43-07.gh-issue-140715.twmcM_.rst b/Misc/NEWS.d/next/Library/2026-02-17-03-43-07.gh-issue-140715.twmcM_.rst new file mode 100644 index 000000000000..3bebc6660df8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-17-03-43-07.gh-issue-140715.twmcM_.rst @@ -0,0 +1 @@ +Add ``%n`` and ``%t`` support to :meth:`~datetime.datetime.strptime`. -- 2.47.3