Batu Lab NotesPractical developer guides

Detect an invalid calendar date from user text

By Batu · English technical notes

Also published in our Blogger archive.

Detect an invalid calendar date from user text

A date-shaped string is not necessarily a calendar date. 2024-02-29 is valid because 2024 is a leap year; 2023-02-29 has the same layout but names a day that does not occur. For an input field that accepts ISO calendar dates, date.fromisoformat() provides both parsing and calendar validation. The example prints a normalized ISO date for accepted input and a stable invalid result when parsing raises ValueError.

The try block is deliberately narrow: only conversion of the user text is expected to fail. Once it returns a date, the assertion checks the concrete leap-day result, while the invalid case is asserted through the printed classification. This avoids trying to reproduce month lengths and leap-year rules by hand.

This function is appropriate only when the product contract is an ISO date. It does not accept reduced-precision dates such as a year and month, and an error can mean malformed syntax as well as an impossible date; show a more specific message only if the application separately validates its accepted syntax. A date also uses Python’s idealized proleptic Gregorian calendar, so it is not a historical-calendar validator. date.fromisoformat() was added in Python 3.7; its accepted formats expanded in Python 3.11. Official date.fromisoformat() documentation

AI-assistance disclosure: AI helped draft this educational example; its assertions demonstrate the two fixed inputs only.

from datetime import date


def classify_iso_date(text):
    try:
        parsed = date.fromisoformat(text)
    except ValueError:
        return f"{text}: invalid"
    return f"{text}: valid ({parsed.isoformat()})"


leap_day = classify_iso_date("2024-02-29")
non_leap_day = classify_iso_date("2023-02-29")

assert leap_day == "2024-02-29: valid (2024-02-29)"
assert non_leap_day == "2023-02-29: invalid"

print(leap_day)
print(non_leap_day)
2024-02-29: valid (2024-02-29)
2023-02-29: invalid