Skip to content

Commit 8d6d10f

Browse files
schedule %e for 3.17, and remove %d now
1 parent 06b6579 commit 8d6d10f

File tree

9 files changed

+73
-49
lines changed

9 files changed

+73
-49
lines changed

Doc/deprecations/pending-removal-in-3.17.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Pending removal in Python 3.17
22
------------------------------
33

4+
* :mod:`datetime`:
5+
6+
* :meth:`~datetime.datetime.strptime` calls using a format string containing
7+
``%e`` (day of month) without a year.
8+
This has been deprecated since Python 3.15.
9+
(Contributed by Stan Ulbrych in :gh:`70647`.)
10+
11+
412
* :mod:`collections.abc`:
513

614
- :class:`collections.abc.ByteString` is scheduled for removal in Python 3.17.

Doc/deprecations/pending-removal-in-3.18.rst

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
Pending removal in Python 3.18
22
------------------------------
33

4-
* :mod:`datetime`:
5-
6-
* :meth:`~datetime.datetime.strptime` calls using a format string containing
7-
a day of month without a year. This has been deprecated since Python 3.13.
8-
94
* No longer accept a boolean value when a file descriptor is expected.
105
(Contributed by Serhiy Storchaka in :gh:`82626`.)
116

Doc/library/datetime.rst

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -606,12 +606,11 @@ Other constructors, all class methods:
606606

607607
.. note::
608608

609-
If *format* specifies a day of month without a year a
610-
:exc:`DeprecationWarning` is emitted. This is to avoid a quadrennial
609+
If *format* specifies a day of month (``%d``) without a year,
610+
:exc:`ValueError` is raised. This is to avoid a quadrennial
611611
leap year bug in code seeking to parse only a month and day as the
612612
default year used in absence of one in the format is not a leap year.
613-
Such *format* values may raise an error as of Python 3.18. The
614-
workaround is to always include a year in your *format*. If parsing
613+
The workaround is to always include a year in your *format*. If parsing
615614
*date_string* values that do not have a year, explicitly add a year that
616615
is a leap year before parsing:
617616

@@ -1180,14 +1179,13 @@ Other constructors, all class methods:
11801179
time tuple. See also :ref:`strftime-strptime-behavior` and
11811180
:meth:`datetime.fromisoformat`.
11821181

1183-
.. versionchanged:: 3.13
1182+
.. versionchanged:: 3.15
11841183

1185-
If *format* specifies a day of month without a year a
1186-
:exc:`DeprecationWarning` is now emitted. This is to avoid a quadrennial
1184+
If *format* specifies a day of month (``%d``) without a year,
1185+
:exc:`ValueError` is raised. This is to avoid a quadrennial
11871186
leap year bug in code seeking to parse only a month and day as the
11881187
default year used in absence of one in the format is not a leap year.
1189-
Such *format* values may raise an error as of Python 3.18. The
1190-
workaround is to always include a year in your *format*. If parsing
1188+
The workaround is to always include a year in your *format*. If parsing
11911189
*date_string* values that do not have a year, explicitly add a year that
11921190
is a leap year before parsing:
11931191

@@ -2919,11 +2917,12 @@ Notes:
29192917
>>> dt.datetime.strptime(f"{month_day};1984", "%m/%d;%Y") # No leap year bug.
29202918
datetime.datetime(1984, 2, 29, 0, 0)
29212919

2922-
.. deprecated-removed:: 3.13 3.18
2920+
.. versionchanged:: 3.15
2921+
Using ``%d`` without a year now raises :exc:`ValueError`.
2922+
2923+
.. deprecated-removed:: 3.15 3.17
29232924
:meth:`~.datetime.strptime` calls using a format string containing
2924-
a day of month without a year now emit a
2925-
:exc:`DeprecationWarning`. In 3.18 we will change this into
2926-
an error or change the default year to a leap year.
2925+
``%e`` without a year now emit a :exc:`DeprecationWarning`.
29272926

29282927
.. rubric:: Footnotes
29292928

Doc/whatsnew/3.15.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,15 @@ collections.abc
14941494
deprecated since Python 3.12, and is scheduled for removal in Python 3.17.
14951495

14961496

1497+
datetime
1498+
--------
1499+
1500+
* :meth:`~datetime.datetime.strptime` now raises :exc:`ValueError` when the
1501+
format string contains ``%d`` (day of month) without a year directive.
1502+
This has been deprecated since Python 3.13.
1503+
(Contributed by Stan Ulbrych and Gregory P. Smith in :gh:`70647`.)
1504+
1505+
14971506
ctypes
14981507
------
14991508

Lib/_strptime.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -464,27 +464,39 @@ def pattern(self, format):
464464
format = re_sub(r'\s+', r'\\s+', format)
465465
format = re_sub(r"'", "['\u02bc]", format) # needed for br_FR
466466
year_in_format = False
467-
day_of_month_in_format = False
467+
day_d_in_format = False
468+
day_e_in_format = False
468469
def repl(m):
469470
directive = m.group()[1:] # exclude `%` symbol
470471
match directive:
471472
case 'Y' | 'y' | 'G':
472473
nonlocal year_in_format
473474
year_in_format = True
474-
case 'd' | 'e':
475-
nonlocal day_of_month_in_format
476-
day_of_month_in_format = True
475+
case 'd':
476+
nonlocal day_d_in_format
477+
day_d_in_format = True
478+
case 'e':
479+
nonlocal day_e_in_format
480+
day_e_in_format = True
477481
return self[directive]
478482
format = re_sub(r'%[-_0^#]*[0-9]*([OE]?[:\\]?.?)', repl, format)
479-
if day_of_month_in_format and not year_in_format:
480-
import warnings
481-
warnings.warn("""\
483+
if not year_in_format:
484+
if day_d_in_format:
485+
raise ValueError(
486+
"Day of month directive '%d' may not be used without "
487+
"a year directive. Parsing dates involving a day of "
488+
"month without a year is ambiguous and fails to parse "
489+
"leap day. Add a year to the input and format. "
490+
"See https://github.com/python/cpython/issues/70647.")
491+
if day_e_in_format:
492+
import warnings
493+
warnings.warn("""\
482494
Parsing dates involving a day of month without a year specified is ambiguous
483-
and fails to parse leap day. The default behavior will change in Python 3.18
484-
to either always raise an exception or to use a different default year.
485-
To avoid trouble, add a specific year to the input and format.""",
486-
DeprecationWarning,
487-
skip_file_prefixes=(os.path.dirname(__file__),))
495+
and fails to parse leap day. '%e' without a year will become an error in Python 3.17.
496+
To avoid trouble, add a specific year to the input and format.
497+
See https://github.com/python/cpython/issues/70647.""",
498+
DeprecationWarning,
499+
skip_file_prefixes=(os.path.dirname(__file__),))
488500
return format
489501

490502
def compile(self, format):

Lib/test/datetimetester.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,14 +1206,18 @@ def test_strptime_single_digit(self):
12061206
newdate = strptime(string, format)
12071207
self.assertEqual(newdate, target, msg=reason)
12081208

1209-
@warnings_helper.ignore_warnings(category=DeprecationWarning)
12101209
def test_strptime_leap_year(self):
1211-
# GH-70647: warns if parsing a format with a day and no year.
1210+
# GH-70647: %d errors if parsing a format with a day and no year.
12121211
with self.assertRaises(ValueError):
12131212
# The existing behavior that GH-70647 seeks to change.
12141213
date.strptime('02-29', '%m-%d')
12151214
with self.assertRaises(ValueError):
12161215
date.strptime('02-29', '%m-%e')
1216+
# %e without a year is deprecated, scheduled for removal in 3.17.
1217+
_strptime._regex_cache.clear()
1218+
with self.assertWarnsRegex(DeprecationWarning,
1219+
r'.*day of month without a year.*'):
1220+
date.strptime('02- 1', '%m-%e')
12171221
with self._assertNotWarns(DeprecationWarning):
12181222
date.strptime('20-03-14', '%y-%m-%d')
12191223
date.strptime('02-29,2024', '%m-%d,%Y')
@@ -3122,15 +3126,15 @@ def test_strptime_single_digit(self):
31223126
newdate = strptime(string, format)
31233127
self.assertEqual(newdate, target, msg=reason)
31243128

3125-
@warnings_helper.ignore_warnings(category=DeprecationWarning)
31263129
def test_strptime_leap_year(self):
3127-
# GH-70647: warns if parsing a format with a day and no year.
3130+
# GH-70647: %d errors if parsing a format with a day and no year.
31283131
with self.assertRaises(ValueError):
31293132
# The existing behavior that GH-70647 seeks to change.
31303133
self.theclass.strptime('02-29', '%m-%d')
3131-
with self.assertWarnsRegex(DeprecationWarning,
3132-
r'.*day of month without a year.*'):
3134+
with self.assertRaises(ValueError):
31333135
self.theclass.strptime('03-14.159265', '%m-%d.%f')
3136+
# %e without a year is deprecated, scheduled for removal in 3.17.
3137+
_strptime._regex_cache.clear()
31343138
with self.assertWarnsRegex(DeprecationWarning,
31353139
r'.*day of month without a year.*'):
31363140
self.theclass.strptime('03-14.159265', '%m-%e.%f')

Lib/test/test_strptime.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -639,15 +639,11 @@ def test_escaping(self):
639639
need_escaping = r".^$*+?{}\[]|)("
640640
self.assertTrue(_strptime._strptime_time(need_escaping, need_escaping))
641641

642-
@warnings_helper.ignore_warnings(category=DeprecationWarning) # gh-70647
643642
def test_feb29_on_leap_year_without_year(self):
644-
time.strptime("Feb 29", "%b %d")
645-
646-
@warnings_helper.ignore_warnings(category=DeprecationWarning) # gh-70647
647-
def test_mar1_comes_after_feb29_even_when_omitting_the_year(self):
648-
self.assertLess(
649-
time.strptime("Feb 29", "%b %d"),
650-
time.strptime("Mar 1", "%b %d"))
643+
with self.assertRaises(ValueError):
644+
time.strptime("Feb 29", "%b %d")
645+
with self.assertRaises(ValueError):
646+
time.strptime("Mar 1", "%b %d")
651647

652648
def test_strptime_F_format(self):
653649
test_date = "2025-10-26"

Lib/test/test_time.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,10 +387,10 @@ def test_strptime_exception_context(self):
387387
self.assertTrue(e.exception.__suppress_context__)
388388

389389
def test_strptime_leap_year(self):
390-
# GH-70647: warns if parsing a format with a day and no year.
391-
with self.assertWarnsRegex(DeprecationWarning,
392-
r'.*day of month without a year.*'):
390+
# GH-70647: %d errors if parsing a format with a day and no year.
391+
with self.assertRaises(ValueError):
393392
time.strptime('02-07 18:28', '%m-%d %H:%M')
393+
# %e without a year is deprecated, scheduled for removal in 3.17.
394394
with self.assertWarnsRegex(DeprecationWarning,
395395
r'.*day of month without a year.*'):
396396
time.strptime('02- 7 18:28', '%m-%e %H:%M')
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
Include the ``%e`` format code in the :meth:`~datetime.datetime.strptime`
2-
deprecation for day-of-month format codes used without a year.
1+
:meth:`~datetime.datetime.strptime` now raises :exc:`ValueError` when the
2+
format string contains ``%d`` without a year directive.
3+
Using ``%e`` without a year now emits a :exc:`DeprecationWarning`.

0 commit comments

Comments
 (0)