skip to content

Why does timedelta.seconds mislead after subtracting two datetime objects, and what should you use instead?

level: middleimportance: must knowfreq 60%

answer

  1. Subtraction gives a duration object
  2. Three normalised integers inside
  3. One attribute is only a remainder
  4. Negative durations keep it positive
  5. The total is a signed float

basics

~20 s

Subtracting two datetime objects returns a timedelta normalised into days, seconds and microseconds. The seconds attribute is only the sub-day remainder, 0 to 86399, so it hides whole days and misreads negatives. Call total_seconds() for the whole duration.

solid answer

~40 s

`datetime - datetime` gives a `timedelta`, and a `timedelta` stores exactly three normalised integers: `days`, `seconds` (always 0-86399) and `microseconds` (0-999999). Every constructor argument, including `weeks` and `hours`, is folded into those three. So a 25-hour gap reports `days == 1, seconds == 3600`, and reading `.seconds` alone reports one hour. Normalisation floors towards negative infinity, so a negative gap of -23 hours becomes `days == -1, seconds == 3600` - `.seconds` is positive even though the duration is negative. `total_seconds()` returns the whole signed duration as a float. When you need an exact integer, floor-divide by a unit: `gap // timedelta(seconds=1)`. Better still, compare durations as `timedelta` objects rather than converting at all.

code

python · 8 lines
python
from datetime import datetime

start = datetime(2026, 3, 1, 10, 0)
end = datetime(2026, 3, 2, 9, 0)
gap = start - end
print(gap)
print(gap.days, gap.seconds, gap.microseconds)
print(gap.total_seconds())

go deeper

for a junior

Recall that subtracting two datetime objects gives a timedelta, and that total_seconds() is how you turn it into a number. Be able to say out loud that .seconds is not the total.

for a middle

Explain the normalised days/seconds/microseconds representation and the flooring rule that keeps seconds non-negative. Show the 25-hour and the negative-gap cases, and give total_seconds() plus floor division by a unit timedelta as the two conversions.

for a senior

Demonstrate the habit that prevents the bug: keep durations as timedelta objects, compare against named timedelta constants, and convert only at the display or serialisation edge. Mention float precision and truncation-versus-flooring when the number must be exact.

for a principal

Own the convention across a codebase: durations typed as timedelta rather than bare ints, units never encoded in variable names or comments, and a single formatting helper for human output. That is what stops unit-confusion defects from recurring team-wide.

## What subtraction actually gives you Subtracting one `datetime` from another produces a `datetime.timedelta`, not a number. A `timedelta` is a *duration* - an amount of elapsed time with no anchor to any calendar date and no time zone. Internally it is exactly three integers, and the constructor normalises whatever you pass into them: - `days` - any integer, positive or negative - `seconds` - always in `0 <= seconds < 86400` - `microseconds` - always in `0 <= microseconds < 1000000` The constructor accepts `weeks`, `days`, `hours`, `minutes`, `seconds`, `milliseconds` and `microseconds`, but it does not *store* them: `timedelta(hours=25)` is stored as `days=1, seconds=3600`. Two `timedelta` objects built from different arguments that describe the same duration are equal and hash the same. ## Why `.seconds` is the classic trap Because `seconds` is capped at one day, it is the *remainder after whole days*, not the duration. A 25-hour outage window reports `seconds == 3600`, and code that logs `gap.seconds` cheerfully reports "1 hour". Nothing raises; the number is simply the wrong number. The second half of the trap is normalisation direction. Python floors towards negative infinity, which keeps `seconds` and `microseconds` non-negative and pushes the sign entirely into `days`: ```python from datetime import datetime gap = datetime(2026, 3, 1, 10) - datetime(2026, 3, 2, 9) # -23 hours print(gap) # -1 day, 1:00:00 print(gap.days, gap.seconds) # -1 3600 print(gap.total_seconds()) # -82800.0 ``` So `.seconds` on a negative duration is positive. Code that branches on `if gap.seconds > 300:` treats "23 hours in the past" as "an hour", and the sign is gone entirely. `str(timedelta)` has the same shape - `'-1 day, 1:00:00'` - which surprises people reading logs, and is why a human-facing duration is usually formatted from `total_seconds()` with `divmod` rather than printed directly. ## `total_seconds()` `total_seconds()` returns `days * 86400 + seconds + microseconds / 10**6` as a **float**, sign included. It is the right default for "how long was this?". Two caveats worth knowing: 1. It is a float. Two durations that differ by a microsecond still compare correctly as `timedelta` objects, but after conversion you are in floating-point territory. For intervals larger than roughly 270 years the float can no longer represent microsecond accuracy - documented, and irrelevant to most services, but the reason the exact form exists. 2. When you want an integer, do not `int(td.total_seconds())` - that truncates towards zero, which is a different rounding than the object's own flooring. Use floor division by a unit `timedelta`, which is exact integer arithmetic on the underlying microseconds: ```python from datetime import timedelta d = timedelta(hours=25, milliseconds=1500) print(d // timedelta(seconds=1)) # 90001 (exact int) print(d / timedelta(minutes=1)) # 1500.025 (float ratio) ``` Dividing a `timedelta` by a `timedelta` gives a plain float ratio; dividing by an int or float gives a `timedelta`; floor-dividing by a `timedelta` gives an int. That trio covers essentially all duration arithmetic without ever converting to raw seconds. ## Prefer comparing durations, not numbers The cleanest fix for most `.seconds` bugs is to stop converting. `timedelta` supports the full ordering and arithmetic set, so a threshold reads better and cannot be misread by unit: ```python from datetime import timedelta STALE_AFTER = timedelta(minutes=15) if gap > STALE_AFTER: # no unit confusion possible refresh() ``` A named `timedelta` constant carries its unit in the constructor keyword, whereas `STALE_AFTER = 900` carries it only in a comment. Deadlines follow the same pattern: `deadline = started_at + timedelta(seconds=30)`, then compare `datetime` objects. ## Other attributes worth knowing - `timedelta.resolution` is one microsecond - the smallest difference the type represents. Sub-microsecond precision does not survive. - `timedelta.min` and `timedelta.max` bound the type at roughly plus or minus 2.7 million years. - `abs(td)` and unary minus work; `td * 2`, `td * 1.5` and `td / 3` all produce `timedelta` results rounded to the nearest microsecond. - Adding a `timedelta` to a `date` gives a `date`; adding one to a `datetime` gives a `datetime`. ## What an interviewer is checking That you know a `timedelta` is a normalised triple rather than a number of seconds, that `.seconds` is a remainder and not a total, that negative durations keep a non-negative `seconds`, and that `total_seconds()` or floor division by a unit `timedelta` is the correct conversion. It is a two-minute answer that reliably separates people who have read the type from people who have only autocompleted it.

  • How would you compare a duration against a fifteen-minute threshold without converting to seconds?
    Compare `timedelta` objects directly: `if gap > timedelta(minutes=15):`. The type supports the full ordering, so the unit lives in the constructor keyword instead of a comment, and no remainder attribute is involved. Define the threshold once as a module-level `timedelta` constant and reuse it for both the comparison and any deadline computed as `started_at + STALE_AFTER`.
  • You need an exact integer number of seconds from a timedelta. Why not int(td.total_seconds())?
    `total_seconds()` is a float, and `int()` truncates towards zero, which disagrees with the object's own flooring for negative durations and can be off by a microsecond-level rounding for large values. `td // timedelta(seconds=1)` is exact integer arithmetic on the stored microseconds and floors consistently. The same pattern gives exact minutes or days by changing the unit.
  • Why does printing a negative timedelta show something like '-1 day, 1:00:00'?
    `str(timedelta)` renders the stored fields, and normalisation floors towards negative infinity, so the sign lives in `days` while `seconds` stays non-negative. It is correct but unfriendly in logs. For human output, format from `total_seconds()` - take `abs()`, `divmod` it into hours, minutes and seconds, and prepend the sign yourself.

It is an odometer with a separate trip counter: the trip counter resets every day, so reading it alone tells you where in the day you are, never how far the journey ran.

saying these in an interview costs you the question

  • Calls .seconds the total length of the duration
  • Thinks .seconds goes negative for a negative gap
  • Believes subtraction returns a float number of seconds
  • Uses int(total_seconds()) and calls it exact
  • Assumes timedelta stores whatever units you passed in

context