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 3ab3450032abe4..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: @@ -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: @@ -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) | @@ -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 d011ddf8b181c3..a83ce0d7820788 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,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 Python 3.15 -to either always raise an exception or to use a different default year (TBD). -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/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)) 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.