Skip to content

The float surface

CountedFloat inherits from float, so every attribute float exposes is part of the counting model's surface — not just the arithmetic operators. This page is the account of that surface, the sibling of the math coverage table: every member of dir(float) participates in exactly one way — counted through an override, reported at WARNING verbosity, or deliberately uncounted with the reason stated below — and a test holds the surface to that partition on every supported interpreter, so an attribute a future CPython adds fails a test instead of becoming a silently uncounted hole.

Counted members

The arithmetic and comparison operators are covered per operation in the FLOP types reference. Beyond those, these members count or preserve countedness:

Member Counts Result
x.real, x.conjugate() (nothing — they return the receiver's own bits, like +x) CountedFloat, bit-identical
x.is_integer() RND + COMP — CPython computes floor(x) == x in doubles, the price of the counted spelling x // 1.0 == x (RND, not F2I: no int materializes) bool
CountedFloat(n), int source I2F — the port's int→double conversion instruction CountedFloat
CountedFloat.from_number(n) (Python 3.14+), int source I2F — the same answer as the constructor, for the same source CountedFloat

The complex-protocol trio gets two answers on purpose: real and conjugate() are receiver-dependent (they hand back the receiver's own bits, so dropping the type would silently stop downstream counting), while imag is receiver-independent+0.0 for every possible receiver, signs and nan payloads included — which makes it a compile-time constant of the port; see its row below.

Reported at WARNING verbosity

x.as_integer_ratio() is uncounted — the port's extraction is a bit-field read plus an integer shift, work outside the floating-point domain the model prices — but its result is the one on this surface that can silently re-enter float arithmetic: n, d = x.as_integer_ratio(); n / d resumes uncounted at zero flops. It is therefore reported through the same once-per-call-site WARNING channel as the uncounted math helpers — see watching what gets counted.

float(x) is not reported, deliberately: it is the documented exit from the counting model, and what makes it safe is that leaving the counted world is the explicit point of the call — unlike a representation query whose parts happen to re-enter.

Deliberately uncounted members

Everything CountedFloat inherits unchanged, with the reason it needs no override:

Member Why it needs no override
__bool__ truthiness is deliberately uncounted bookkeeping: the interpreter inserts it implicitly at every if/while/and/or/not/assert with no opt-out, and python -O elides assert entirely, so a count here would vary with interpreter flags; the algorithmic spelling x != 0.0 counts COMP
__float__ the documented escape hatch: an explicit, uncounted exit from the counting model -- double->double identity, no instruction in the port. Safe because it is explicit at the call site, which is exactly what the implicit property access real lacks
__format__ formatting produces a str that leaves the algorithm; correctly-rounded decimal conversion is the machinery round(x, n) declares unmodeled -- except the '%' presentation type, which scales by 100 in binary64 first: one MUL, a labeled uncounted exception (unobservable from Python, and the result cannot re-enter the algorithm)
__getformat__ structurally not a float operation: binds to the type and reads no value (its return string is endianness-dependent, so only existence is pinned)
__getnewargs__ pickling (protocol 2+) and copy/deepcopy: hands new a plain-float 1-tuple, so round-trips rebuild a CountedFloat at zero count -- deserialization is not the algorithm converting an integer
fromhex preserves countedness: CPython wraps the parsed double through the subclass constructor, so the result is a CountedFloat; the strtod-shaped parse itself has no FlopType -- a stated gap (CountedFloat(3) costs I2F where fromhex('0x1.8p+1') costs nothing)
hex produces a str that leaves the algorithm; the hex digits are the mantissa's own nibbles, so the port emits no floating-point instruction -- the remaining work is integer/bit manipulation, which this library counts nowhere
imag plain +0.0 for every receiver, signs and nan payloads included: a compile-time constant of the port, per the cost model's constant-result convention (the receiver-dependent real / conjugate preserve countedness instead)
__class__ (object plumbing) object plumbing: computes no float value
__delattr__ (object plumbing) refuses attribute mutation (empty slots), matching plain float
__dir__ (object plumbing) object plumbing: computes no float value
__getattribute__ (object plumbing) object plumbing: computes no float value
__getstate__ (object plumbing) pickling: returns None because slots is empty -- no instance state exists to serialize; pinned because it guards the slots decision
__init__ (object plumbing) object.init no-op: the narrowed new is the sole arity gate, so CountedFloat(1.0, 2.0) fails in new before this ever runs
__reduce__ (object plumbing) pickling: delegates through copyreg and rebuilds a CountedFloat at zero count; reachable only by direct call (pickle routes through reduce_ex)
__reduce_ex__ (object plumbing) pickling (protocols 0-1): routes through copyreg and float(), rebuilding a CountedFloat at zero count with no CountedFloat override participating -- pinned because that route is pure CPython machinery
__setattr__ (object plumbing) refuses attribute mutation (empty slots), matching plain float
__sizeof__ (object plumbing) reports memory, computes no float value (the byte count is build-dependent -- 3.14t differs -- so only existence is pinned)
__str__ (object plumbing) presentation: float defines no str of its own, so str(x) falls through object.str to the loud CountedFloat.repr -- the single presentation mechanism; provenance is pinned because a future float-defined str would silently change what str(x) prints
__subclasshook__ (object plumbing) object plumbing: computes no float value

The presentation contract

__repr__ is the single presentation mechanism, and it is deliberately loud: repr(x), str(x), print(x), f"{x}" and format(x, "") all render CountedFloat(1.5)float defines no __str__ of its own, so every empty-spec spelling falls through to the repr. Whether a value silently fell out of the counting system is this library's central hazard, and the repr is the zero-cost place it shows.

Any non-empty format spec formats the plain value instead — f"{x:.2f}" gives 1.50, and even a bare alignment (f"{x:>10}") flips to plain rendering — so reports and tables keep their layout. The loud/quiet boundary is exactly spec == "".

One formatting subtlety is a labeled exception rather than a derivation: the % presentation type multiplies by 100 in binary64 before converting (f"{x:.1%}"), and that one MUL is uncounted — it is unobservable from Python, and the resulting str cannot re-enter the algorithm.

Pickling and copying

Pickling, copy.copy and copy.deepcopy round-trip a CountedFloat to a CountedFloat at zero count, on both mechanisms pickle uses: protocols 2+ rebuild through __getnewargs__ and the class's own constructor, protocols 0–1 through copyreg and plain CPython machinery. The zero count is correct — deserialization is not the algorithm converting an integer — and bit-exactness holds from protocol 1 up (protocol 0 stores floats as repr text, which loses nan payloads; plain-float behavior, not a CountedFloat difference).