apache/beam · error · ValueError

Cannot convert from nanoseconds to microseconds because…

Error message

Cannot convert from nanoseconds to microseconds because this loses precision. Please make sure that this is the correct behavior you want and manually truncate the precision to the nearest microseconds. See [https://github.com/apache/beam/issues/19922] for more information.

What it means

Duration.from_proto() converts a google.protobuf duration_pb2.Duration (seconds + nanos) into a Beam Duration, which only has microsecond resolution. If the proto's nanos field is not a multiple of 1000, the conversion would lose precision, so a ValueError is raised; the message links to beam issue 19922 about better defining duration precision.

Solutions

  1. Round/truncate the proto before converting: set nanos = (nanos // 1000) * 1000, or construct Duration directly with Duration(micros=(proto.seconds*1_000_000 + proto.nanos//1000)).
  2. Fix the producer so durations are emitted with microsecond resolution.
  3. If the caller controls the source, use Timestamp/Duration APIs end-to-end instead of passing nanosecond protos.
  4. Catch ValueError around from_proto and handle the lossy conversion explicitly for your domain.

Example fix

// before
d = Duration.from_proto(duration_pb2.Duration(seconds=1, nanos=123456789))
// after
proto = duration_pb2.Duration(seconds=1, nanos=123456789)
proto.nanos = (proto.nanos // 1000) * 1000  # truncate to micros
d = Duration.from_proto(proto)
Defensive patterns

Strategy: validation

Validate before calling

def from_proto_safe(proto):
    if proto.nanos % 1000 != 0:
        proto = duration_pb2.Duration(seconds=proto.seconds,
                                      nanos=(proto.nanos // 1000) * 1000)
    return Duration.from_proto(proto)

Type guard

def proto_is_micro_aligned(proto):
    return proto.nanos % 1000 == 0

Try / catch

try:
    d = Duration.from_proto(proto)
except ValueError:
    d = Duration(micros=proto.seconds * 1_000_000 + proto.nanos // 1000)

Prevention

When it happens

Trigger: Calling Duration.from_proto(proto) where proto.nanos % 1000 != 0, e.g. protos built from external systems with nanosecond durations (proto.FromTimedelta with nanos, or hand-built duration_pb2.Duration(seconds=1, nanos=123)).

Common situations: Decoding durations from gRPC/protobuf payloads produced by services that emit true nanosecond durations; converting from other timestamp libraries; test fixtures with arbitrary nanos values.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/dc733a1d031756e3. Report an issue: GitHub.

Appendix: source

Thrown at sdks/python/apache_beam/utils/timestamp.py:536

  def to_proto(self) -> duration_pb2.Duration:
    """Returns the `google.protobuf.duration_pb2` representation."""
    secs = self.micros // 1000000
    nanos = (self.micros % 1000000) * 1000
    return duration_pb2.Duration(seconds=secs, nanos=nanos)

  @staticmethod
  def from_proto(duration_proto: duration_pb2.Duration) -> 'Duration':
    """Creates a Duration from a `google.protobuf.duration_pb2`.

    Note that the google has a sub-second resolution of nanoseconds whereas this
    class has a resolution of microsends. This class will truncate the
    nanosecond resolution down to the microsecond.
    """

    if duration_proto.nanos % 1000 != 0:
      # TODO(https://github.com/apache/beam/issues/19922): Better define
      # durations.
      raise ValueError(
          "Cannot convert from nanoseconds to microseconds " +
          "because this loses precision. Please make sure that " +
          "this is the correct behavior you want and manually " +
          "truncate the precision to the nearest microseconds. " +
          "See [https://github.com/apache/beam/issues/19922] for " +
          "more information.")

    return Duration(
        seconds=duration_proto.seconds, micros=duration_proto.nanos // 1000)

  def __repr__(self) -> str:
    micros = self.micros
    sign = ''
    if micros < 0:
      sign = '-'
      micros = -micros
    int_part = micros // 1000000
    frac_part = micros % 1000000

View on GitHub (pinned to 12126d8942)