Numbers, Booleans, None & Numeric Precision

Numeric calculations in Python involve distinct memory models: arbitrary-precision integers, IEEE 754 double-precision binary floats, exact decimal representations, and rational fractions. Choosing the right numeric type is a correctness decision before it is a performance optimization.

This chapter details CPython’s integer PyLongObject architecture, IEEE 754 floating-point mechanics, fixed-point Decimal contexts, and the historical implementation of bool as an integer subclass.


1. CPython Arbitrary-Precision Integers (PyLongObject)

Unlike C, Java, or Rust—where integers overflow at 32 or 64 bits—CPython integers (int) have arbitrary precision. A Python integer grows dynamically in memory as large as available system RAM allows.

CPython represents integers using PyLongObject:

// CPython 3.11+ Internal Representation
struct _longobject {
    PyObject_HEAD
    size_t long_value_header; // Stores sign and digit count
    digit ob_digit[1];        // Array of 30-bit digits (uint32_t)
};

On 64-bit systems, CPython allocates integers using base-$2^30$ digits:

  • Numbers fitting in 30 bits ($< 1,073,741,824$) use 1 digit payload (28 bytes total).
  • Larger numbers allocate additional 30-bit ob_digit array slots dynamically.
PyLongObject Memory Layout (64-bit CPython):

[ ob_refcnt (8B) | ob_type (8B) | long_value_header (8B) ]
                                |
                                v
                   [ ob_digit: 30-bit digit array ]
                   [ Slot 0: 2^0..2^29 ] [ Slot 1: 2^30..2^59 ] ...

2. IEEE 754 Floating-Point Mechanics (float)

Python’s float is implemented directly as a C double (64-bit IEEE 754 binary floating-point number):

64-Bit IEEE 754 Binary Float Structure:

[ 1 Bit Sign | 11 Bits Exponent | 52 Bits Mantissa / Fraction ]

Because binary floating-point cannot represent base-10 fractions (like $0.1$ or $0.2$) exactly in binary powers of two, standard floating-point arithmetic introduces representation error:

print(0.1 + 0.2)          # 0.30000000000000004
print(0.1 + 0.2 == 0.3)  # False!

Floating-Point Comparison Protocol:

Never test computed floats using ==. Always use math.isclose() with explicit relative and absolute tolerances (rel_tol, abs_tol).


3. Financial Exactness: decimal.Decimal & fractions.Fraction

When domain requirements demand exact base-10 arithmetic (such as financial transactions or tax calculations), use decimal.Decimal:

  • Construct from Strings: Always instantiate Decimal from strings or integers (Decimal("0.1")), never from a float (Decimal(0.1)), which would copy the binary approximation into the Decimal context.
  • Configurable Rounding Context: Set explicit precision and rounding rules (e.g. ROUND_HALF_UP or ROUND_HALF_EVEN).
from decimal import Decimal, ROUND_HALF_UP

subtotal = Decimal("19.99")
tax = (subtotal * Decimal("0.0825")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(tax)  # 1.65 (Exact financial calculation)

4. Historical Subclassing: bool and None

  • bool is a Subclass of int: In CPython’s C source code, PyBool_Type.tp_base is set to &PyLong_Type. True and False are singleton PyLongObject instances with values 1 and 0. Consequently, True + True evaluates to 2 in arithmetic contexts, though relying on this is an anti-pattern.
  • NoneType Singleton: None is a singleton instance of NoneType (PyNone). Identity check x is None directly compares the raw memory pointer to &_Py_NoneStruct.
Display Options
Appearance
Text Size
100%