Numbers & numeric types
Mid / SeniorArbitrary-precision int, IEEE 754 floats, Decimal vs Fraction, floor division, numeric hashing and promotion.
Created 23 October 2026
7 concepts · detail hidden
01How arbitrary-precision `int` is implemented, and its performance cost
#c-how-arbitrary-precision-int-is-implemented-and-its-performanCPython represents integers internally as an array of fixed-size “digits,” stored in an internal base close to
2^30(an implementation detail, notably not base-10 — nothing to do with how the number displays) — arithmetic on large integers is implemented as genuine multi-digit operations, conceptually similar to how a person would perform long multiplication by hand across multiple digit “columns,” rather than a single hardware instruction. This means operations on genuinely large Python integers cost measurably more than a single CPU instruction would for a fixed-width Cint/long; even small Python integers still structurally pay some of this same general overhead, just operating on a trivially small digit array — part of why Python integer arithmetic is inherently somewhat slower than equivalent raw C integer arithmetic, even for small everyday numbers.02Why `0.1 + 0.2 == 0.3` is `False` — IEEE 754 binary representation, mechanically
#c-why-01-02-03-is-false-ieee-754-binary-representation-mechaniPython’s
floattype is an IEEE 754 double-precision binary floating-point number — a specific, standardized way of representing real numbers in binary with finite precision. Most everyday decimal fractions, including0.1, have no exact, finite representation in binary (the same underlying reason1/3has no exact finite decimal representation) — so0.1is actually stored internally as the closest representable binary approximation to it, not the literal exact value. These tiny representation errors don’t cancel out perfectly across subsequent arithmetic, so0.1 + 0.2produces a result that’s extremely close to, but not bit-for-bit identical to, the separately-computed closest binary approximation of0.3— henceFalse. The practical fix:decimal.Decimalfor genuinely exact decimal arithmetic (essential for financial calculations), or comparing floats using a deliberate tolerance (math.isclose) rather than exact==for general floating-point comparisons.03`decimal.Decimal` vs `fractions.Fraction` vs `float` — a scenario for each
#c-decimaldecimal-vs-fractionsfraction-vs-float-a-scenario-for-decimal.Decimal: financial/monetary calculations specifically needing exact base-10 arithmetic with precisely controllable rounding rules — a plainfloatsilently introduces binary-representation rounding error that genuinely compounds and accumulates across many sequential transactions, which is unacceptable for money.fractions.Fraction: situations genuinely needing exact rational-number arithmetic, with no rounding whatsoever, not merely improved decimal precision —Fraction(1, 3)is stored and manipulated exactly, whereas evenDecimal('0.333...')still fundamentally isn’t precisely equal to one-third, no matter how many digits you carry. Plainfloat: performance-sensitive numeric or scientific computation where small representation error is genuinely acceptable and raw computational speed actually matters — usingDecimalinside a genuinely tight numerical loop would introduce a real, measurable performance regression compared to native floating-point arithmetic.04Floor division and modulo with negative operands — Python floors, C truncates
#c-floor-division-and-modulo-with-negative-operands-python-flooPython’s
//(floor division) always rounds toward negative infinity — genuine mathematical flooring, not truncation toward zero — and%(modulo) is deliberately defined so that the identity(a // b) * b + (a % b) == aalways holds exactly. Concretely:-7 // 2gives-4(not-3), and-7 % 2gives1(matching the divisor’s sign, per this definition) — this genuinely surprises developers coming from C or Java, where integer division instead truncates toward zero (-7 / 2gives-3in C) and the modulo result’s sign can differ from Python’s. This is a real, common source of subtle off-by-one and sign-related bugs specifically when porting numeric code between Python and C-family languages.05Why `hash(1) == hash(1.0) == hash(True)` — and the collapsed-dict-keys gotcha
#c-why-hash1-hash10-hashtrue-and-the-collapsed-dict-keys-gotchaPython enforces a strict language-level invariant: any two objects that compare equal via
==must also produce the exact same hash value — this is required fordict/setto function correctly at all, since they rely fundamentally on hash values to locate entries efficiently. Since1 == 1.0 == Trueall genuinely hold true in Python, they must all hash identically too — meaning{1: "a"}[1.0]and{1: "a"}[True]both correctly, successfully retrieve"a", since these are all treated as fundamentally the same key by the dict’s internal machinery. A genuinely real, surprising gotcha this produces: a dict literal written as{1: "int", 1.0: "float", True: "bool"}does not end up with three separate entries — it ends up with exactly one, since each subsequent key that happens to be equal to an already-seen one simply overwrites that same underlying slot in place.06`complex` numbers — the limited everyday relevance, and the `j` suffix convention
#c-complex-numbers-the-limited-everyday-relevance-and-the-j-sufcomplexis genuinely rare outside scientific computing, signal processing, or specific numerical algorithms (FFT-based computation being a common example). The basic literal syntax is3+4j; Python usesj(noti) as its imaginary-unit suffix — a convention borrowed specifically from electrical engineering notation, whereiis already reserved for electrical current, rather than from general mathematical convention.07Mixing `int` and `float` in arithmetic — numeric promotion via reflected-operator dispatch
#c-mixing-int-and-float-in-arithmetic-numeric-promotion-via-refPython automatically promotes the overall result to the “wider”/more general involved type —
int + floatproduces afloat— and, genuinely worth knowing, this isn’t implemented via some separate, special-cased “numeric type coercion” subsystem at all: it’s driven by the exact same reflected-operator dispatch machinery that underlies operator overloading generally.int.__add__(some_float)correctly recognizes it doesn’t know how to natively combine with afloatoperand and returnsNotImplemented;float.__radd__then picks up that fallback and actually performs the real mixed-type arithmetic, correctly returning afloat. “Numeric promotion” is really just this general operator-dispatch protocol at work, not a separate, special-cased mechanism.