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:
date objects>>> 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:
for i in x.
In this statement, the Python interpreter calls the iter(x).
It first check if x.__iter__() is implemented if NOT it will use x.__getitem__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']
in operator to check if a date is in the range.>>> "2025-12-02" in dr
True
>>> "2025-12-06" in dr
False
random.choice().>>> import random
>>> random.choice(dr)
'2025-12-04'
Feel like to try it out yourself? The code is available in this file.
__len__ and __getitem__, we can make our user-defined classes behave like built-in Python types.len(), iteration, slicing, and membership testing.reversed() and random.choice() to avoid reinventing the wheel.