From c079c2a4b3bdf9521beaa2093c620dcba3137dff Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 6 Feb 2026 20:00:25 +0000 Subject: [PATCH 1/8] Add deprecation for %e with no year --- Doc/library/datetime.rst | 6 +++--- Lib/_strptime.py | 6 +++--- Lib/test/datetimetester.py | 8 ++++++++ Lib/test/test_time.py | 7 +++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 3ab3450032abe4..e092fedef5b2af 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2531,13 +2531,13 @@ requires, and these work on all supported platforms. | | truncated to an integer as a | | | | | zero-padded decimal number. | | | +-----------+--------------------------------+------------------------+-------+ -| ``%d`` | Day of the month as a | 01, 02, ..., 31 | \(9) | -| | zero-padded decimal number. | | | +| ``%d`` | Day of the month as a | 01, 02, ..., 31 | \(9), | +| | zero-padded decimal number. | | \(10 | +-----------+--------------------------------+------------------------+-------+ | ``%D`` | Equivalent to ``%m/%d/%y``. | 11/10/2025 | \(9), | | | | | \(0) | +-----------+--------------------------------+------------------------+-------+ -| ``%e`` | The day of the month as a | ␣1, ␣2, ..., 31 | | +| ``%e`` | The day of the month as a | ␣1, ␣2, ..., 31 | \(10) | | | space-padded decimal number. | | | +-----------+--------------------------------+------------------------+-------+ | ``%F`` | Equivalent to ``%Y-%m-%d``, | 2025-10-11, | \(0) | diff --git a/Lib/_strptime.py b/Lib/_strptime.py index d011ddf8b181c3..12a1bea904c225 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -466,7 +466,7 @@ def repl(m): case 'Y' | 'y' | 'G': nonlocal year_in_format year_in_format = True - case 'd': + case 'd' | 'e': nonlocal day_of_month_in_format day_of_month_in_format = True return self[directive] @@ -475,8 +475,8 @@ def repl(m): import warnings warnings.warn("""\ Parsing dates involving a day of month without a year specified is ambiguous -and fails to parse leap day. The default behavior will change in Python 3.15 -to either always raise an exception or to use a different default year (TBD). +and fails to parse leap day. The default behavior will change in a future Python +version to either always raise an exception or to use a different default year. To avoid trouble, add a specific year to the input & format. See https://github.com/python/cpython/issues/70647.""", DeprecationWarning, diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 8d39299b3ff442..2693ef2cc0c654 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1208,9 +1208,12 @@ def test_strptime_leap_year(self): with self.assertRaises(ValueError): # The existing behavior that GH-70647 seeks to change. date.strptime('02-29', '%m-%d') + with self.assertRaises(ValueError): + date.strptime('02-29', '%m-%e') with self._assertNotWarns(DeprecationWarning): date.strptime('20-03-14', '%y-%m-%d') date.strptime('02-29,2024', '%m-%d,%Y') + date.strptime('02-29,2024', '%m-%e,%Y') class SubclassDate(date): sub_var = 1 @@ -3096,10 +3099,15 @@ def test_strptime_leap_year(self): with self.assertWarnsRegex(DeprecationWarning, r'.*day of month without a year.*'): self.theclass.strptime('03-14.159265', '%m-%d.%f') + with self.assertWarnsRegex(DeprecationWarning, + r'.*day of month without a year.*'): + self.theclass.strptime('03-14.159265', '%m-%e.%f') with self._assertNotWarns(DeprecationWarning): self.theclass.strptime('20-03-14.159265', '%y-%m-%d.%f') with self._assertNotWarns(DeprecationWarning): self.theclass.strptime('02-29,2024', '%m-%d,%Y') + with self._assertNotWarns(DeprecationWarning): + self.theclass.strptime('02-29,2024', '%m-%e,%Y') def test_strptime_z_empty(self): for directive in ('z', ':z'): diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index c360f4a64c266b..41fce8fb5c6067 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -358,11 +358,11 @@ def test_strptime(self): # Should be able to go round-trip from strftime to strptime without # raising an exception. tt = time.gmtime(self.t) - for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'H', 'I', + for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'e', 'H', 'I', 'j', 'm', 'M', 'p', 'S', 'U', 'w', 'W', 'x', 'X', 'y', 'Y', 'Z', '%'): format = '%' + directive - if directive == 'd': + if directive in ('d', 'e'): format += ',%Y' # Avoid GH-70647. strf_output = time.strftime(format, tt) try: @@ -391,6 +391,9 @@ def test_strptime_leap_year(self): with self.assertWarnsRegex(DeprecationWarning, r'.*day of month without a year.*'): time.strptime('02-07 18:28', '%m-%d %H:%M') + with self.assertWarnsRegex(DeprecationWarning, + r'.*day of month without a year.*'): + time.strptime('02- 7 18:28', '%m-%e %H:%M') def test_asctime(self): time.asctime(time.gmtime(self.t)) From dd067c13270597f8add7004116d0bea61faab6a1 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 7 Feb 2026 13:02:12 +0000 Subject: [PATCH 2/8] Change to Python 3.18 --- Doc/deprecations/pending-removal-in-3.18.rst | 5 +++++ Doc/library/datetime.rst | 10 +++++----- Lib/_strptime.py | 7 +++---- .../2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst | 2 ++ 4 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst diff --git a/Doc/deprecations/pending-removal-in-3.18.rst b/Doc/deprecations/pending-removal-in-3.18.rst index 3e799219478424..b3613225138f1a 100644 --- a/Doc/deprecations/pending-removal-in-3.18.rst +++ b/Doc/deprecations/pending-removal-in-3.18.rst @@ -1,6 +1,11 @@ Pending removal in Python 3.18 ------------------------------ +* :mod:`datetime`: + + * :meth:`~datetime.datetime.strptime` calls using a format string containing + a day of month without a year. This has been deprecated since Python 3.13. + * :mod:`decimal`: * The non-standard and undocumented :class:`~decimal.Decimal` format diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index e092fedef5b2af..f460f3ca53a95a 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1168,7 +1168,7 @@ Other constructors, all class methods: :exc:`DeprecationWarning` is now emitted. This is to avoid a quadrennial leap year bug in code seeking to parse only a month and day as the default year used in absence of one in the format is not a leap year. - Such *format* values may raise an error as of Python 3.15. The + Such *format* values may raise an error as of Python 3.18. The workaround is to always include a year in your *format*. If parsing *date_string* values that do not have a year, explicitly add a year that is a leap year before parsing: @@ -2532,7 +2532,7 @@ requires, and these work on all supported platforms. | | zero-padded decimal number. | | | +-----------+--------------------------------+------------------------+-------+ | ``%d`` | Day of the month as a | 01, 02, ..., 31 | \(9), | -| | zero-padded decimal number. | | \(10 | +| | zero-padded decimal number. | | \(10) | +-----------+--------------------------------+------------------------+-------+ | ``%D`` | Equivalent to ``%m/%d/%y``. | 11/10/2025 | \(9), | | | | | \(0) | @@ -2868,11 +2868,11 @@ Notes: >>> datetime.strptime(f"{month_day};1984", "%m/%d;%Y") # No leap year bug. datetime.datetime(1984, 2, 29, 0, 0) - .. deprecated-removed:: 3.13 3.15 + .. deprecated-removed:: 3.13 3.18 :meth:`~.datetime.strptime` calls using a format string containing a day of month without a year now emit a - :exc:`DeprecationWarning`. In 3.15 or later we may change this into - an error or change the default year to a leap year. See :gh:`70647`. + :exc:`DeprecationWarning`. In 3.18 we will change this into + an error or change the default year to a leap year. .. rubric:: Footnotes diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 12a1bea904c225..a83ce0d7820788 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -475,10 +475,9 @@ def repl(m): import warnings warnings.warn("""\ Parsing dates involving a day of month without a year specified is ambiguous -and fails to parse leap day. The default behavior will change in a future Python -version to either always raise an exception or to use a different default year. -To avoid trouble, add a specific year to the input & format. -See https://github.com/python/cpython/issues/70647.""", +and fails to parse leap day. The default behavior will change in Python 3.18 +to either always raise an exception or to use a different default year. +To avoid trouble, add a specific year to the input and format.""", DeprecationWarning, skip_file_prefixes=(os.path.dirname(__file__),)) return format diff --git a/Misc/NEWS.d/next/Library/2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst b/Misc/NEWS.d/next/Library/2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst new file mode 100644 index 00000000000000..d4a16a96b19503 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst @@ -0,0 +1,2 @@ +Include the ``%e`` format code in the :meth:`~datetime.datetime.strptime` +deprecation for day-of-month format codes used without a year. From fba84f07d14b41d3bf01cde5d22d722ccee98fc5 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 7 Feb 2026 20:34:14 +0000 Subject: [PATCH 3/8] date.strptime note --- Doc/library/datetime.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index f460f3ca53a95a..e4913287ff420d 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -599,7 +599,7 @@ Other constructors, all class methods: :exc:`DeprecationWarning` is emitted. This is to avoid a quadrennial leap year bug in code seeking to parse only a month and day as the default year used in absence of one in the format is not a leap year. - Such *format* values may raise an error as of Python 3.15. The + Such *format* values may raise an error as of Python 3.18. The workaround is to always include a year in your *format*. If parsing *date_string* values that do not have a year, explicitly add a year that is a leap year before parsing: From a5ce1692d6765cb5f90265d39fe0a97c6b485a6c Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 9 Feb 2026 17:18:37 +0000 Subject: [PATCH 4/8] Fix conflict "fix" --- Lib/test/test_time.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index ee15f641cb7f5f..3a99525dbfbb07 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -358,13 +358,8 @@ def test_strptime(self): # Should be able to go round-trip from strftime to strptime without # raising an exception. tt = time.gmtime(self.t) -<<<<<<< e-d-no-year - for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'e', 'H', 'I', - 'j', 'm', 'M', 'p', 'S', -======= - for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'F', 'H', 'I', + for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'e', 'F', 'H', 'I', 'j', 'm', 'M', 'p', 'S', 'T', ->>>>>>> main 'U', 'w', 'W', 'x', 'X', 'y', 'Y', 'Z', '%'): format = '%' + directive if directive in ('d', 'e'): From 8d6d10fc6f08de4788eb0215067126474863366c Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 14 Apr 2026 17:00:23 +0100 Subject: [PATCH 5/8] schedule `%e` for 3.17, and remove `%d` now --- Doc/deprecations/pending-removal-in-3.17.rst | 8 +++++ Doc/deprecations/pending-removal-in-3.18.rst | 5 --- Doc/library/datetime.rst | 25 +++++++------ Doc/whatsnew/3.15.rst | 9 +++++ Lib/_strptime.py | 36 ++++++++++++------- Lib/test/datetimetester.py | 16 +++++---- Lib/test/test_strptime.py | 12 +++---- Lib/test/test_time.py | 6 ++-- ...6-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst | 5 +-- 9 files changed, 73 insertions(+), 49 deletions(-) diff --git a/Doc/deprecations/pending-removal-in-3.17.rst b/Doc/deprecations/pending-removal-in-3.17.rst index e769c9d371e133..ea9fb93ddd8c84 100644 --- a/Doc/deprecations/pending-removal-in-3.17.rst +++ b/Doc/deprecations/pending-removal-in-3.17.rst @@ -1,6 +1,14 @@ Pending removal in Python 3.17 ------------------------------ +* :mod:`datetime`: + + * :meth:`~datetime.datetime.strptime` calls using a format string containing + ``%e`` (day of month) without a year. + This has been deprecated since Python 3.15. + (Contributed by Stan Ulbrych in :gh:`70647`.) + + * :mod:`collections.abc`: - :class:`collections.abc.ByteString` is scheduled for removal in Python 3.17. diff --git a/Doc/deprecations/pending-removal-in-3.18.rst b/Doc/deprecations/pending-removal-in-3.18.rst index 77726743b3e273..eb42fe9919eaeb 100644 --- a/Doc/deprecations/pending-removal-in-3.18.rst +++ b/Doc/deprecations/pending-removal-in-3.18.rst @@ -1,11 +1,6 @@ Pending removal in Python 3.18 ------------------------------ -* :mod:`datetime`: - - * :meth:`~datetime.datetime.strptime` calls using a format string containing - a day of month without a year. This has been deprecated since Python 3.13. - * No longer accept a boolean value when a file descriptor is expected. (Contributed by Serhiy Storchaka in :gh:`82626`.) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index e135bac8aa8653..f3c4ef9199075c 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -606,12 +606,11 @@ Other constructors, all class methods: .. note:: - If *format* specifies a day of month without a year a - :exc:`DeprecationWarning` is emitted. This is to avoid a quadrennial + If *format* specifies a day of month (``%d``) without a year, + :exc:`ValueError` is raised. This is to avoid a quadrennial leap year bug in code seeking to parse only a month and day as the default year used in absence of one in the format is not a leap year. - Such *format* values may raise an error as of Python 3.18. The - workaround is to always include a year in your *format*. If parsing + The workaround is to always include a year in your *format*. If parsing *date_string* values that do not have a year, explicitly add a year that is a leap year before parsing: @@ -1180,14 +1179,13 @@ Other constructors, all class methods: time tuple. See also :ref:`strftime-strptime-behavior` and :meth:`datetime.fromisoformat`. - .. versionchanged:: 3.13 + .. versionchanged:: 3.15 - If *format* specifies a day of month without a year a - :exc:`DeprecationWarning` is now emitted. This is to avoid a quadrennial + If *format* specifies a day of month (``%d``) without a year, + :exc:`ValueError` is raised. This is to avoid a quadrennial leap year bug in code seeking to parse only a month and day as the default year used in absence of one in the format is not a leap year. - Such *format* values may raise an error as of Python 3.18. The - workaround is to always include a year in your *format*. If parsing + The workaround is to always include a year in your *format*. If parsing *date_string* values that do not have a year, explicitly add a year that is a leap year before parsing: @@ -2919,11 +2917,12 @@ Notes: >>> dt.datetime.strptime(f"{month_day};1984", "%m/%d;%Y") # No leap year bug. datetime.datetime(1984, 2, 29, 0, 0) - .. deprecated-removed:: 3.13 3.18 + .. versionchanged:: 3.15 + Using ``%d`` without a year now raises :exc:`ValueError`. + + .. deprecated-removed:: 3.15 3.17 :meth:`~.datetime.strptime` calls using a format string containing - a day of month without a year now emit a - :exc:`DeprecationWarning`. In 3.18 we will change this into - an error or change the default year to a leap year. + ``%e`` without a year now emit a :exc:`DeprecationWarning`. .. rubric:: Footnotes diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index c754b634ecccfa..0ae38d371e09ce 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1494,6 +1494,15 @@ collections.abc deprecated since Python 3.12, and is scheduled for removal in Python 3.17. +datetime +-------- + +* :meth:`~datetime.datetime.strptime` now raises :exc:`ValueError` when the + format string contains ``%d`` (day of month) without a year directive. + This has been deprecated since Python 3.13. + (Contributed by Stan Ulbrych and Gregory P. Smith in :gh:`70647`.) + + ctypes ------ diff --git a/Lib/_strptime.py b/Lib/_strptime.py index b7b27a22a17252..746b0907c1d9f4 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -464,27 +464,39 @@ def pattern(self, format): format = re_sub(r'\s+', r'\\s+', format) format = re_sub(r"'", "['\u02bc]", format) # needed for br_FR year_in_format = False - day_of_month_in_format = False + day_d_in_format = False + day_e_in_format = False def repl(m): directive = m.group()[1:] # exclude `%` symbol match directive: case 'Y' | 'y' | 'G': nonlocal year_in_format year_in_format = True - case 'd' | 'e': - nonlocal day_of_month_in_format - day_of_month_in_format = True + case 'd': + nonlocal day_d_in_format + day_d_in_format = True + case 'e': + nonlocal day_e_in_format + day_e_in_format = True return self[directive] format = re_sub(r'%[-_0^#]*[0-9]*([OE]?[:\\]?.?)', repl, format) - if day_of_month_in_format and not year_in_format: - import warnings - warnings.warn("""\ + if not year_in_format: + if day_d_in_format: + raise ValueError( + "Day of month directive '%d' may not be used without " + "a year directive. Parsing dates involving a day of " + "month without a year is ambiguous and fails to parse " + "leap day. Add a year to the input and format. " + "See https://github.com/python/cpython/issues/70647.") + if day_e_in_format: + import warnings + warnings.warn("""\ Parsing dates involving a day of month without a year specified is ambiguous -and fails to parse leap day. The default behavior will change in Python 3.18 -to either always raise an exception or to use a different default year. -To avoid trouble, add a specific year to the input and format.""", - DeprecationWarning, - skip_file_prefixes=(os.path.dirname(__file__),)) +and fails to parse leap day. '%e' without a year will become an error in Python 3.17. +To avoid trouble, add a specific year to the input and format. +See https://github.com/python/cpython/issues/70647.""", + DeprecationWarning, + skip_file_prefixes=(os.path.dirname(__file__),)) return format def compile(self, format): diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 889ce9a456bc6d..e261195ca1ff91 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1206,14 +1206,18 @@ def test_strptime_single_digit(self): newdate = strptime(string, format) self.assertEqual(newdate, target, msg=reason) - @warnings_helper.ignore_warnings(category=DeprecationWarning) def test_strptime_leap_year(self): - # GH-70647: warns if parsing a format with a day and no year. + # GH-70647: %d errors if parsing a format with a day and no year. with self.assertRaises(ValueError): # The existing behavior that GH-70647 seeks to change. date.strptime('02-29', '%m-%d') with self.assertRaises(ValueError): date.strptime('02-29', '%m-%e') + # %e without a year is deprecated, scheduled for removal in 3.17. + _strptime._regex_cache.clear() + with self.assertWarnsRegex(DeprecationWarning, + r'.*day of month without a year.*'): + date.strptime('02- 1', '%m-%e') with self._assertNotWarns(DeprecationWarning): date.strptime('20-03-14', '%y-%m-%d') date.strptime('02-29,2024', '%m-%d,%Y') @@ -3122,15 +3126,15 @@ def test_strptime_single_digit(self): newdate = strptime(string, format) self.assertEqual(newdate, target, msg=reason) - @warnings_helper.ignore_warnings(category=DeprecationWarning) def test_strptime_leap_year(self): - # GH-70647: warns if parsing a format with a day and no year. + # GH-70647: %d errors if parsing a format with a day and no year. with self.assertRaises(ValueError): # The existing behavior that GH-70647 seeks to change. self.theclass.strptime('02-29', '%m-%d') - with self.assertWarnsRegex(DeprecationWarning, - r'.*day of month without a year.*'): + with self.assertRaises(ValueError): self.theclass.strptime('03-14.159265', '%m-%d.%f') + # %e without a year is deprecated, scheduled for removal in 3.17. + _strptime._regex_cache.clear() with self.assertWarnsRegex(DeprecationWarning, r'.*day of month without a year.*'): self.theclass.strptime('03-14.159265', '%m-%e.%f') diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index dfc8ef6d2c5b7e..0f4107319a1047 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -639,15 +639,11 @@ def test_escaping(self): need_escaping = r".^$*+?{}\[]|)(" self.assertTrue(_strptime._strptime_time(need_escaping, need_escaping)) - @warnings_helper.ignore_warnings(category=DeprecationWarning) # gh-70647 def test_feb29_on_leap_year_without_year(self): - time.strptime("Feb 29", "%b %d") - - @warnings_helper.ignore_warnings(category=DeprecationWarning) # gh-70647 - def test_mar1_comes_after_feb29_even_when_omitting_the_year(self): - self.assertLess( - time.strptime("Feb 29", "%b %d"), - time.strptime("Mar 1", "%b %d")) + with self.assertRaises(ValueError): + time.strptime("Feb 29", "%b %d") + with self.assertRaises(ValueError): + time.strptime("Mar 1", "%b %d") def test_strptime_F_format(self): test_date = "2025-10-26" diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 2c2de56642b6b9..21df425c5e1fe2 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -387,10 +387,10 @@ def test_strptime_exception_context(self): self.assertTrue(e.exception.__suppress_context__) def test_strptime_leap_year(self): - # GH-70647: warns if parsing a format with a day and no year. - with self.assertWarnsRegex(DeprecationWarning, - r'.*day of month without a year.*'): + # GH-70647: %d errors if parsing a format with a day and no year. + with self.assertRaises(ValueError): time.strptime('02-07 18:28', '%m-%d %H:%M') + # %e without a year is deprecated, scheduled for removal in 3.17. with self.assertWarnsRegex(DeprecationWarning, r'.*day of month without a year.*'): time.strptime('02- 7 18:28', '%m-%e %H:%M') diff --git a/Misc/NEWS.d/next/Library/2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst b/Misc/NEWS.d/next/Library/2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst index d4a16a96b19503..9fd39743ca58bf 100644 --- a/Misc/NEWS.d/next/Library/2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst +++ b/Misc/NEWS.d/next/Library/2026-02-07-12-54-20.gh-issue-70647.Bja_Lk.rst @@ -1,2 +1,3 @@ -Include the ``%e`` format code in the :meth:`~datetime.datetime.strptime` -deprecation for day-of-month format codes used without a year. +:meth:`~datetime.datetime.strptime` now raises :exc:`ValueError` when the +format string contains ``%d`` without a year directive. +Using ``%e`` without a year now emits a :exc:`DeprecationWarning`. From 8fd2015e707bdee768d0cd723a1bfac2696b948e Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 14 Apr 2026 17:02:41 +0100 Subject: [PATCH 6/8] typo --- Lib/test/test_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 21df425c5e1fe2..1850f053aaffd6 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -393,7 +393,7 @@ def test_strptime_leap_year(self): # %e without a year is deprecated, scheduled for removal in 3.17. with self.assertWarnsRegex(DeprecationWarning, r'.*day of month without a year.*'): - time.strptime('02- 7 18:28', '%m-%e %H:%M') + time.strptime('02-07 18:28', '%m-%e %H:%M') def test_asctime(self): time.asctime(time.gmtime(self.t)) From d0f15db203d8c5024f88259ade0f85756f144455 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 14 Apr 2026 17:03:41 +0100 Subject: [PATCH 7/8] Ruff --- Lib/test/datetimetester.py | 2 +- Lib/test/test_strptime.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e261195ca1ff91..71b0fc85db5845 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -22,7 +22,7 @@ from test import support from test.support import is_resource_enabled, ALWAYS_EQ, LARGEST, SMALLEST -from test.support import os_helper, script_helper, warnings_helper +from test.support import os_helper, script_helper import datetime as datetime_module from datetime import MINYEAR, MAXYEAR diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index 0f4107319a1047..5ac28870455f4d 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -8,7 +8,6 @@ import platform import sys from test import support -from test.support import warnings_helper from test.support import skip_if_buggy_ucrt_strfptime, run_with_locales from datetime import date as datetime_date From 37e47a43bc4f120e4a1b279ffb287cb846308a6c Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 14 Apr 2026 17:41:00 +0100 Subject: [PATCH 8/8] fix tests --- Lib/test/datetimetester.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 71b0fc85db5845..5d5b8e415f3cd2 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1211,13 +1211,11 @@ def test_strptime_leap_year(self): with self.assertRaises(ValueError): # The existing behavior that GH-70647 seeks to change. date.strptime('02-29', '%m-%d') - with self.assertRaises(ValueError): - date.strptime('02-29', '%m-%e') # %e without a year is deprecated, scheduled for removal in 3.17. _strptime._regex_cache.clear() with self.assertWarnsRegex(DeprecationWarning, r'.*day of month without a year.*'): - date.strptime('02- 1', '%m-%e') + date.strptime('02-01', '%m-%e') with self._assertNotWarns(DeprecationWarning): date.strptime('20-03-14', '%y-%m-%d') date.strptime('02-29,2024', '%m-%d,%Y')