Python: fix ValueError: day of month directive '%d' may not be used without a year directive

It’s about time they made this deprecation.

Take this code, parsing the date of an annually recurring event that only has a month and day, such as a work anniversary:

from datetime import datetime


def parse_anniversary(date_str: str) -> datetime:
    parsed = datetime.strptime(date_str, "%d/%m")
    return parsed.day, parsed.month

With Python 3.13 or 3.14, run it on January 1st, and it will log a warning, but still parse the date:

>>> parse_anniversary("01/01")
/.../example.py:7: DeprecationWarning: 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.
  parsed = datetime.strptime(date_str, "%d/%m")
(1, 1)

Note the returned value’s year is 1900.

But, parse February 29th, and the warning is followed by a ValueError:

>>> parse_anniversary("29/02")
/.../example.py:7: DeprecationWarning: Parsing dates involving a day of month without a year specified is ambiguous
and fails to parse leap day...
Traceback (most recent call last):
...
ValueError: day is out of range for month

That ValueError isn’t new: without a year, strptime() has always filled in 1900 behind the scenes, which isn’t a leap year, so parsing “29 February” has always failed, on every Python version. The new part is the warning, telling us this whole approach is on its way out.

On Python 3.15+, any call to strptime() with a %d in the format string, without a year, will raise a ValueError immediately, no matter the input:

>>> parse_anniversary("01/01")
Traceback (most recent call last):
...
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.

From the release notes:

strptime() now raises 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.)

It seems like a fair trade-off to break this usage completely, rather than let it continue to hide around in code bases and only fail in the rare case that it is handed February 29th. But it does mean any such call sites need fixing…

The fix

Add a year to both the format string and input. If you do have a specific year, use it, otherwise add a leap year (2000 works) and ignore the year in the returned value:

from datetime import datetime


def parse_anniversary(date_str: str) -> datetime:
    date_str = f"{date_str}/2000"  # Add leap year to input
    parsed = datetime.strptime(date_str, "%d/%m/%Y")
    return parsed.day, parsed.month

This will work fine on all Python versions for all dates, including February 29th:

>>> parse_anniversary("01/01")
(1, 1)
>>> parse_anniversary("29/02")
(29, 2)

Fin

May all of your directives be unambiguous,

—Adam


😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: