rlim
Main image for Building a Pythonic DateRange: Elegant Iteration Over Dates

Building a Pythonic DateRange: Elegant Iteration Over Dates

written by Ricky Lim on 2025-12-10

During my coding journey, I need a range-like functionality that could iterate over dates. Unfortunately, Python standard library does not yet provide such functionality. Also pulling-in third-party libraries was not an option for my use case. Then, I delegated to my coding assistant.

Here is my prompt:

Implement python solution to iterate each date given the start_date and end_date. Both in the format "YYYY-MM-DD" and inclusive. Provide verified doctests that demonstrate expected behavior.

The solution that my coding assistant provided is as below, I omitted the doctest for brevity.


def date_range(start_date: str, end_date: str) -> Iterator[str]:
    """
    Generate dates between start_date and end_date (inclusive).

    Args:
        start_date: ISO format date string "YYYY-MM-DD"
        end_date: ISO format date string "YYYY-MM-DD"

    Yields:
        str: Date strings in ISO format "YYYY-MM-DD"

    """
    start = date.fromisoformat(start_date)
    end = date.fromisoformat(end_date)

    return (
        (start + timedelta(days=n)).isoformat()
        for n in range((end - start).days + 1)
    )

I'm quite satisfied with the solution. The implementation is straightforward. Also, I kind of like the solution using generator expression to save memory.

I confirmed that the implementation works as expected by running the provided doctests:

$ python -m doctest date_utils.py -v
Trying:
    list(date_range("2025-12-01", "2025-12-05"))
Expecting:
    ['2025-12-01', '2025-12-02', '2025-12-03', '2025-12-04', '2025-12-05']
ok
Trying:
    list(date_range("2025-12-31", "2026-01-02"))
Expecting:
    ['2025-12-31', '2026-01-01', '2026-01-02']
ok
Trying:
    list(date_range("2024-02-28", "2024-03-01"))
Expecting:
    ['2024-02-28', '2024-02-29', '2024-03-01']
ok
Trying:
    list(date_range("2025-12-07", "2025-12-07"))
Expecting:
    ['2025-12-07']
ok
Trying:
    len(list(date_range("2025-01-01", "2025-12-31")))
Expecting:
    365
ok
Trying:
    gen = date_range("2025-12-01", "2025-12-03")
Expecting nothing
ok
Trying:
    next(gen)
Expecting:
    '2025-12-01'
ok
Trying:
    list(gen)  # consumes remaining items
Expecting:
    ['2025-12-02', '2025-12-03']
ok

The tests confirm the expected behaviour I was aiming for, and the implementation is simple.

So next, let's improve it to be more pythonic by implementing a DateRange class.

class DateRange:
    """
    Iterator class for date ranges between start_date (inclusive) and end_date (inclusive)
    """
    def __init__(self, start_date: str, end_date: str):
        self.start = date.fromisoformat(start_date)
        self.end = date.fromisoformat(end_date)

        if self.start > self.end:
            raise ValueError(
                f"start_date ({start_date}) must be <= end_date ({end_date})"
            )
        self._dates = [
            (self.start + timedelta(days=n)).isoformat()
            for n in range((self.end - self.start).days + 1)
        ]

    def __len__(self) -> int:
        return len(self._dates)

    def __getitem__(self, index: int) -> str:
        return self._dates[index]

A special method that we are familiar with is __init__. This method is used to initialize our user-defined class. We call them special methods as it's meant to be called by the Python Interpreter and not by us. As such we don't call DateRange.__init__() but we use DateRange() and Python will then call __init__.

Our __init__ methods does the following:

>>> dr = DateRange("2025-12-01", "2025-12-05")

Two other special methods that we implemented to harness the power of python is __len__ and __getitem__. Thanks to composition, our __len__ and __getitem__ simply delegate to the underlying list of dates we built in __init__.

The trade-off here is that we use more memory to store the list of dates, compared to our previous generator-based function date_range. When the date ranges are usually not in the order of millions, this is acceptable.

__len__

This special method allows us to call our class with built-in len() to check the size over the date range.

>>> len(dr)
5

__getitem__

Implementing __getitem__ unlocks powerful functionality in Python such as:

With this implementation we can iterate over the date range. It is using the x.getitem() under the hood.:

>>> for d in dr:
...    print(d)
2025-12-01
2025-12-02
2025-12-03
2025-12-04
2025-12-05

Also we can iterate in reverse by using reversed() built-in function:

>>> for d in reversed(dr):
...    print(d)
2025-12-05
2025-12-04
2025-12-03
2025-12-02
2025-12-01
# The first three dates
>>> dr[:3]
['2025-12-01', '2025-12-02', '2025-12-03']
# Dates from index 1 to 2
>>> dr[1:2]
['2025-12-02']
>>> "2025-12-02" in dr
True
>>> "2025-12-06" in dr
False
>>> import random
>>> random.choice(dr)
'2025-12-04'

Feel like to try it out yourself? The code is available in this file.

Key Takeaways