#500·boltons

daterange with a month step raises 'day is out of range for month' when start is on the 29th-31st

Author: Sreekant13Created Sep 16, 2026Updated Sep 16, 2026

timeutils.daterange with a month-based step raises ValueError: day is out of range for month whenever start falls on a day that does not exist in a later month (the 29th–31st). A monthly sequence anchored to a month-end date is a normal thing to ask for, and it crashes instead of yielding.

Repro (current main)

python
from datetime import date
from boltons.timeutils import daterange

list(daterange(date(2020, 1, 31), date(2020, 6, 30), step=(0, 1, 0), inclusive=True))
# ValueError: day is out of range for month

Same for date(2020, 3, 31), date(2021, 1, 31), date(2020, 1, 30), etc. — any month-step that lands the anchor day on a shorter month. It raises on the very first advance, so the generator yields nothing usable.

Cause

_advance keeps the day fixed and only replaces year and month:

python
def _advance(cur):
    if m_step:
        m_y_step, cur_month = divmod((cur.month - 1) + m_step, 12)
        cur = cur.replace(year=cur.year + m_y_step,
                          month=(cur_month + 1))       # <- day is carried over unchanged
    return cur + d_step

date(2020, 1, 31).replace(month=2) is date(2020, 2, 31), which does not exist, so replace raises. There is no day validation or clamping.

Which behaviour do you want?

The crash is clearly a bug, but the fix is a design choice, so I would rather ask than presume:

  1. Clamp to the last valid day of the target month (what dateutil.relativedelta does): Jan 31 -> Feb 29 -> Mar 29 -> .... Simple, never raises. Downside: after the first clamp the day "drifts" (it does not snap back to the 31st).
  2. Snap to month-end when the anchor was a month-end: Jan 31 -> Feb 29 -> Mar 31 -> Apr 30 -> .... Matches what most people mean by "the last of every month", but more logic and only well-defined when the anchor is itself a month-end.
  3. Raise a clear, documented error (e.g. "daterange month step cannot start from day 29-31") instead of the current opaque date error, and document the limitation.

My inclination is option 1 for its simplicity and precedent, with the drift called out in the docstring; option 2 if you would rather preserve month-end anchoring. Happy to open a PR with tests for whichever you prefer (or the clear-error version).

For context: existing daterange issues #319 (month 12 / year-step off-by-one), #297 (infinite loop when start == stop) and PR #432 (non-advancing step) are all separate; none touch the day-overflow path.