Skip to content

Suprising behavior with start_seconds constrution due to floating point #61

Description

@nickswalker
timecode = Timecode("30000/1001", "09:06:40;28")
float_roundtrip = Timecode("30000/1001", start_seconds=timecode.float)
print(float_roundtrip) # 09:06:40;27

Confusion about construction from floats is common enough that in the past the generally incorrect solution of simply rounding up has been proposed:
#22

It's especially unfortunate when it comes from floats produced by the library itself though. Since these issues are inherent to floating point arithmetic, what do you think about modifying/removing the .float getter in favor of e.g. a Decimal representation? That results in expected behavior for the above snippet. Users can unwrap the Decimal into a float at their own peril, but then it should be less surprising when they encounter strange behavior.

@property
def float(self) -> float:
    """Return the seconds as float.

    Returns:
        float: The seconds as float.
    """
    return Decimal(self.frames) / Decimal(self._int_framerate)

There may also still be the need to use the fractional frame representation when available (#36). Happy to send a PR.

Activity

  1. nickswalker commented on Jul 9, 2025

    @nickswalker
    Author

    Testing this more thoroughly, short of removing .float in favor of a rational getter returning a Fraction, a small epsilon is the only robust way to smooth over this behavior.

  2. added a commit that references this issue on Jul 9, 2025
    588fd66
  3. eoyilmaz commented on Jul 9, 2025

    @eoyilmaz
    Owner

    I quite like the idea of using Decimal in place of float to be honest. But this is a major change and on top of my head I can't evaluate how much work it requires.

    I wanted to modernise this library already, so maybe we can do a new major release that is not concerned about being backwards compatible etc.

  4. nickswalker commented on Jul 10, 2025

    @nickswalker
    Author

    Using the smallest Decimal patch (as above) under default settings, the results of the int truncation later on work out much worse for some framerates:

    Percentage incorrect float roundtrip by implementation across 00:00:00:00-24:00:00:00

    Frame Rate Decimal Implementation Float Implementation
    23.976 13.6% 0.0%
    23.98 13.6% 0.0%
    24 13.6% 0.0%
    25 0.0% 6.8%
    29.97 10.0% 2.0%
    30 10.0% 2.0%
    50 0.0% 6.8%
    59.94 2.9% 2.0%
    60 2.9% 2.0%
    ms 0.0% 0.9%

    Example: frame 8 under 60fps is at time 8/60, or 0.1333... seconds. There is no amount of finite precision that can avoid the truncating step of int(seconds * framerate) returning 7 instead of 8, so the Decimal implementation will exhibit this problem no matter what:

    timecode = Timecode(60, "00:00:00:07")
    float_roundtrip = Timecode(60, start_seconds=timecode.float)
    print(float_roundtrip) # 00:00:006
    

    This case (and many others, per the table) happen to work with float due to built in round-to-nearest behavior. So the ways to avoid problems with this strange behavior, in order of disruptive-ness:

    1. Document and warn in the constructor doc string
    2. Add an epsilon to the float returned by the getter (Shift timecode floats forward a few picoseconds #63).
    3. Add an epsilon during construction from a float. About the same, but it would have the effect of correcting sketchy float iteration like in Construction using seconds is not accurate #22, which could theoretically be bad if someone relied on it.
    4. Discourage the use of the float representation of a timecode, e.g. by removing the float property, and instead providing a rational representation like Fraction. It'd be easy to manipulate for many use cases, helping users avoid loss of precision until they absolutely need to. Construction from the Fraction would be exact, but users with floats from other sources would still need to be warned.
    5. Change from truncating to rounding. A big change in the library's semantics that would break many valid uses, while technically fixing this behavior as a side effect.

    I think 1,2, and 3 might all be okay and worthwhile without a major release. 4 requires a bit more thought to see what the value in use would be, but clearly removing .float would be a major change. 5 does not seem worth considering.

  5. cubicibo commented on Jul 10, 2025

    @cubicibo

    3: unacceptable because it goes against the user intent by changing the value behind their back.
    5: does not solve the issue, as a truncation may be desirable too (#48 ...)

    I have no strong opinion on 4. Internally the library should generalize the usage of fractions... but you are screwed the moment the user goes with a non-standard decimal framerate. 1 is fair as SMPTE documents also warns about possible rounding issues.

  6. added a commit that references this issue on Dec 31, 2025
    627eca7
  7. added a commit that references this issue on Jan 27, 2026
    e5fc4f7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions