Read the type of each result carefully. The whole exercise is knowing that the answer to a subtraction is not a number.
from datetime import date
start = date.fromisoformat('2026-01-01')
end = date.fromisoformat('2026-03-01')
gap = end - start
print(gap) # -> 59 days, 0:00:00 a timedelta
print(gap.days) # -> 59 an int
print(type(gap)) # -> <class 'datetime.timedelta'>
- fromisoformat parses and validates in one call
It turns the text into a real date and raises ValueError if the text is not one — wrong separators, month 13, the 31st of February. You get the conversion and the validation from the same line, which is why no hand-written checking is needed here.
- Subtracting dates gives a duration, not a count
date minus date is a timedelta: a span of time that knows about days, seconds and microseconds. Reaching for .days is what turns it into the integer the caller asked for. Returning the timedelta itself would be a different, and usually more useful, function — but not this one.
- The calendar comes free
February’s length, leap years, the 30-versus-31 day months and the boundary at New Year are all handled by the date type. Every one of those is a bug you would otherwise write yourself, and the reason "just multiply by 30" fails in production every December.
Do not work with the strings. Convert both into date objects first, and see what Python lets you do with two of those.
date.fromisoformat(text) turns 'YYYY-MM-DD' into a date and raises ValueError on anything else. Subtracting two dates gives a timedelta.
Parse both, subtract end minus start — that order, so backwards spans come out negative — and return the .days attribute of the result. It is one line, and there is no validation to write.
from datetime import date def days_between(start, end): return (date.____(end) - date.____(start)).____
from datetime import date
def days_between(start, end):
return (date.fromisoformat(end) - date.fromisoformat(start)).days
The first thing the function does is stop dealing in text, and everything after that is real date arithmetic. That ordering is the whole design: convert once, as early as possible, and let the type carry the rules. Code that keeps values as strings and converts repeatedly is code that will eventually convert one of them wrong.
There is no check for the format, the month range, the day range or the leap year, and yet both invalid cases pass. fromisoformat raises ValueError for all of them with a better message than you would have written. Letting a well-chosen built-in do the refusing is nearly always better than hand-rolling the rules.
Subtracting the other way round would make every answer’s sign backwards, and wrapping it in abs() would hide direction the caller may need — 'this invoice is 30 days overdue' and 'due in 30 days' are not the same sentence. One of the cases exists purely to pin this down.
This counts calendar days, which is not what people usually mean by 'how long'. Business days need a working-day calendar and public holidays that differ by country. And the moment times and timezones are involved, whole days stop being well defined at all: two timestamps 25 hours apart can fall on the same calendar date in one zone and two days apart in another, and daylight saving means some days are 23 or 25 hours long. For anything time-of-day sensitive, work in aware datetimes and be explicit about which zone the question is being asked in.
- Return whole weeks as well, and decide what a partial week at the end should do.
- Write business_days_between that skips Saturdays and Sundays, then notice how much harder public holidays make it and where that list would have to come from.
- Accept a date object as well as a string, so callers who already parsed do not have to convert back — and work out how to tell the two apart cleanly.