# When does negative zero compare equal to zero but produce different serialized output? **Date of Record:** 2026 November 2 **Scope:** Applies to systems that serialize floating-point zero across a boundary that also performs equality comparisons, where -0.0 and 0.0 are treated as equivalent by the comparator but differently by the serializer. Excludes integer-only paths and systems that canonicalize zeros at construction. ## Core Distinction **Predicate.** IEEE-754 defines -0.0 == 0.0 as true, but `copysign` reveals the sign bit is preserved. Serializers differ: some emit `-0.0`, some emit `0.0`, some emit `-0`. A comparison passes; a byte-level equality check fails. **Failure.** A computed value reaches -0.0 (underflow, negation of zero, multiplication by negative). It compares equal to 0.0 in code, so validation passes. Serialized as JSON, it emits `-0.0`, which some parsers reject and others preserve. A cache key built from the serialized bytes diverges between the two. **Asymmetry.** The equality check is correct. The serializer is correct. The cache key is correct. Divergence exists only when a downstream consumer recomputes equality from the serialized form and gets `-0.0 == 0.0` but not `"-0.0" == "0.0"`. ## What Was Observed **Signature.** Cache misses for keys that compare equal. Duplicate rows with values that `==` says are the same but `hash()` says are different (in languages where `hash(-0.0) != hash(0.0)`). Serialized payloads differing only in a leading minus sign. **Trace.** A pricing calculation returns `-0.0` for a zero-dollar discount. `hash(-0.0) == hash(0.0)` in Python but `str(-0.0) == '-0.0'`. A dedup key built from `str(value)` treats the discount and a true zero as distinct keys, creating two records for the same economic event. **Why monitoring stays green.** The comparison `-0.0 == 0.0` returns True, so any check that uses `==` passes. Only byte-level equality between serialized forms reveals the divergence. ## Condition of Validity Zero handling is consistent only while at least one holds: 1. **Canonicalization.** Every zero is normalized to `+0.0` at the earliest boundary, and the sign is not propagated. 2. **Byte-equality.** No system compares serialized forms of floats as bytes; deduplication and caching are performed on the numeric value with canonicalized zero. 3. **Type substitution.** Zero is represented as an integer (0) or as a distinct absent value, never as a signed float at a serialization boundary. Condition 1 is the structural fix; 2 and 3 are compensations. ## Decision Boundary - Canonicalized to +0.0: safe. - Byte-equality comparison over uncanonicalized serialized floats: unsafe. - Serialization boundary preserving -0.0 into a cache key: unsafe. - Integer zeros in JSON: safe, but converts the schema from float to int. ## Operational Effect The defect is small, rare, and easy to dismiss as a curiosity until a reconciliation fails on a zero-dollar row. Remediation is canonicalization. The trap is to test with `==`, which cannot observe the problem. Every test that expects to catch this must compare serialized bytes. ```python import math from dataclasses import dataclass class SignedZeroError(ValueError): """Raised when -0.0 and 0.0 will produce different serialized bytes downstream.""" @dataclass(frozen=True) class ZeroGate: value: float canonicalize: bool = False def verify(self) -> bool: if self.canonicalize: return True if self.value == 0.0 and math.copysign(1.0, self.value) < 0: raise SignedZeroError( "-0.0 compares equal to 0.0 but serializes differently: byte-equality " "caches and dedup keys will diverge" ) return True def dispatch_zero_handling(gate: ZeroGate) -> str: try: gate.verify() except SignedZeroError: return "canonicalize" return "as-computed" ```