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.
.. 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.15. 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:
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.15. 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:
| | 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/28/25 | \(9) |
| | | | |
+-----------+--------------------------------+------------------------+-------+
-| ``%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, | |
>>> 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.15
+ .. 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.15 or later we may change this into
- an error or change the default year to a leap year. See :gh:`70647`.
+ ``%e`` without a year now emit a :exc:`DeprecationWarning`.
.. rubric:: Footnotes
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
------
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:
nonlocal year_in_format
year_in_format = True
case 'd':
- nonlocal day_of_month_in_format
- day_of_month_in_format = True
+ 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.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.
+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__),))
+ DeprecationWarning,
+ skip_file_prefixes=(os.path.dirname(__file__),))
return format
def compile(self, format):
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
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')
+ # %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-01', '%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
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.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-%d.%f')
+ 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'):
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
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"
# 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', 'D', 'F', 'H', 'I',
+ for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'D', 'e', 'F', 'H', 'I',
'j', 'm', 'M', 'n', 'p', 'S', 't', 'T',
'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:
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.
+ # 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-07 18:28', '%m-%d %H:%M')
+ time.strptime('02-07 18:28', '%m-%e %H:%M')
def test_asctime(self):
time.asctime(time.gmtime(self.t))
--- /dev/null
+: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`.