I wanted to write a human readable datetime.timedelta that can be used in log files.

Eg, "Report issued 1 hour, 44 minutes, 20 seconds ago"

I noticed that casting a timedelta to str() generates something almost like what I want, but not quite.

To this end I wrote this:

def verbose_timedelta(delta):
hours, remainder = divmod(delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
dstr = "%s day%s" % (delta.days, "s"[delta.days==1:])
hstr = "%s hour%s" % (hours, "s"[hours==1:])
mstr = "%s minute%s" % (minutes, "s"[minutes==1:])
sstr = "%s second%s" % (seconds, "s"[seconds==1:])
dhms = [dstr, hstr, mstr, sstr]
for x in range(len(dhms)):
if not dhms[x].startswith('0'):
dhms = dhms[x:]
break
dhms.reverse()
for x in range(len(dhms)):
if not dhms[x].startswith('0'):
dhms = dhms[x:]
break
dhms.reverse()
return ', '.join(dhms)


Essentially, it's shaving off both ends of a list to make the results more meaningful.

The code above feels clunky though. Is there a more "Pythonic" way to do it? I'm using Python 2.7.3.

## migrated from stackoverflow.comDec 13 '13 at 5:37

This question came from our site for professional and enthusiast programmers.

• Use xrange instead of range and str.format instead of the % operator. But these are just side notes ;) – jazzpi Nov 26 '13 at 16:12
• You can also use "s" if hours==1 else "" instead of "s"[hours==1:]. This goes with the "Explicit is better than implicit." Python idiom. – jorispilot Nov 26 '13 at 16:16
• Thanks. The main part I wanted to clear up was the two for loops and the double reversing. This seems like something python could do in a single line. Maybe. Hmm. – Paul Nov 26 '13 at 16:44

• Use a ternary operator in "s"[seconds==1:].
• Use a generator expression to replace the xstr = "%s... lines.
• The two for loops should use enumerate(), i.e. they could be for s, i in range(...).
• The two for loops should be moved into a for _ in range(2):.
• i is preferred over x when using an i​ncrementing i​ndex counter.
• The filtering of the redundant strings which the for-loop does could be done earlier so that the number to string code can be modified but the filtering code will not require adjustments.

PS: I have implemented a similar function here:

    days, rem = divmod(seconds, 86400)
hours, rem = divmod(rem, 3600)
minutes, seconds = divmod(rem, 60)
if seconds < 1:seconds = 1
locals_ = locals()
magnitudes_str = ("{n} {magnitude}".format(n=int(locals_[magnitude]), magnitude=magnitude)
for magnitude in ("days", "hours", "minutes", "seconds") if locals_[magnitude])
eta_str = ", ".join(magnitudes_str)

• I really like this because it's teaching me how to get under the belly of Python. However, it doesn't quite do what I would like. Or what my function above does. Your code could return something like this: "7 days, 5 seconds" Ie because hours and minutes aren't set they're not shown. I don't like this, but purely on aesthetic grounds. For me, intervening zero time intervals should be shown, eg "7 days, 0 hours, 6 minutes" not "7 days, 6 minutes". – Paul Nov 26 '13 at 16:42
• @Paul Yes, it doesn't do exactly the same thing as yours. I should update it to shave off both the ends like yours does. – Ramchandra Apte Nov 27 '13 at 5:03

Pythonic is in the eye of the beholder, but here's my stab at it:

def verbose_timedelta(delta):
d = delta.days
h, s = divmod(delta.seconds, 3600)
m, s = divmod(s, 60)
labels = ['day', 'hour', 'minute', 'second']
dhms = ['%s %s%s' % (i, lbl, 's' if i != 1 else '') for i, lbl in zip([d, h, m, s], labels)]
for start in range(len(dhms)):
if not dhms[start].startswith('0'):
break
for end in range(len(dhms)-1, -1, -1):
if not dhms[end].startswith('0'):
break
return ', '.join(dhms[start:end+1])

• Cool, looks like it might fall foul to "0 hour ago". Plural exists on zero quantities. – Paul Nov 27 '13 at 18:39
• @Paul: Changed i > 1 to i != 1 to address zero quantities. – Steven Rumbalski Nov 27 '13 at 18:42