Skip to main content

Command Palette

Search for a command to run...

04_Numbers in Python

Understanding How Python Represents and Works With Numbers

Published
10 min read
04_Numbers in Python
S

Full-stack developer documenting what I’m learning as I go. This space is all about tech, understanding how things work, and writing things down as they start to make sense.

In lower-level languages like C or Java, a number is often just a direct mapping to a processor register (a 32-bit or 64-bit block of memory). In Python, however, numbers are abstractions—they are fully-fledged objects allocated on the heap.

We’ll dive into how Python represents and handles numbers internally, including its numeric type hierarchy, support for arbitrary-precision arithmetic, bit-level operations, and underlying memory management.


1: Introduction to Python Numbers

Because everything in Python is an object, a simple integer like 42 carries significant metadata. It is not just raw binary data; instead, it is a heap-allocated object that contains a reference count (used by CPython’s memory management), a pointer to the int type definition, and a variable-sized payload to store the actual value.

Note: The internal details discussed here refer specifically to CPython, the reference implementation of Python.

1.1 The PyObject Overhead

When you define x = 42, CPython allocates an object that is conceptually similar to the following C structure:

struct _longobject {
    Py_ssize_t ob_refcnt;   // Reference count
    PyTypeObject *ob_type;  // Type pointer
    Py_ssize_t ob_size;     // Size and sign information (simplified)
    digit ob_digit[1];      // The actual numerical value
};

This structure is a simplified representation.
Modern CPython versions use macros (PyObject_HEAD) and a packed field (lv_tag) internally, but the conceptual model remains the same.

The key idea is that Python integers:

  • Are allocated on the heap

  • Store metadata alongside the value

  • Support arbitrary-precision arithmetic using a variable-length digit array

We can inspect this overhead using the sys module:

import sys

x = 42
print(f"Type: {type(x)}")                   # <class 'int'>
print(f"Memory Address: {hex(id(x))}")      # e.g., 0x7ff...
print(f"Size in RAM: {sys.getsizeof(x)} bytes") # 28 bytes

1.2 The Numeric Type Hierarchy

numbers.Number (Abstract Base Class)
├── numbers.Integral
│   └── int
│       └── bool (True=1, False=0)
├── numbers.Real
│   └── float
└── numbers.Complex
    └── complex

Python strictly categorizes numbers using Abstract Base Classes (ABCs) in the numbers module.

  • numbers.Number: The root of all numeric types.

  • numbers.Integral: Includes int and bool(which is a subclass of int).

  • numbers.Real: Includes float.

  • numbers.Complex: Includes complex.


2: Integers (int)

Python 3 replaced the old long type with a unified int type that supports arbitrary-precision arithmetic. This means integers can grow as large as the available memory allows, rather than being limited to a fixed number of bits.

Unlike many low-level languages, Python integers do not overflow; instead, they expand to accommodate larger values.

2.1 Arbitrary Precision Mechanism

In standard fixed-width arithmetic, exceeding the limit of a 64-bit integer (2^63 − 1 for signed integers) results in overflow. Python avoids this by dynamically allocating additional “digits” (blocks of memory) to store larger numbers internally.

# Standard integer
small = 42

# A number larger than the universe's atom count
# Python handles this natively without special libraries
huge = 10 ** 100 
print(f"Digits in huge number: {len(str(huge))}") # 101 digits

2.2 Integer Caching (Interning)

To optimize memory, Python pre-creates and caches integer objects in the range of -5 to 256.

  • If you create a variable x = 100, it points to the pre-existing cached object.

  • For integers outside this range, a new object is typically created.

# Cached range (-5 to 256)
a = 100
b = 100
print(a is b)  # True (Same memory address)

# Outside cached range
x = 1000
y = 1000
print(x is y)  # False (Different objects)
print(x == y)  # True (Same value)

3: Floating-Point Numbers (float)

Floating-point numbers are Python’s way of representing real numbers—values that contain decimal points. Under the hood, Python’s float type maps directly to double-precision (64-bit) floating-point numbers in C and follows the IEEE 754 standard, which is the same format used by most modern processors.

This design choice makes floating-point arithmetic fast and hardware-accelerated, but it also introduces important limitations that every developer should understand.

3.1 The Representation Error

Unlike integers, floating-point numbers are stored in binary, not decimal. This means that many decimal values we use daily cannot be represented exactly in memory.

A useful analogy:

  • In decimal, 1/3 = 0.333... (infinite repetition)

  • In binary, 1/10 = 0.0001100110011... (also infinite repetition)

Because the representation is infinite, Python stores the closest possible approximation.in binary.

# The classic floating point error
val = 0.1 + 0.2
print(f"Result: {val}")          # 0.30000000000000004
print(f"Is equal to 0.3? {val == 0.3}")   # False

This behavior often confuses beginners, but it is not a Python bug. It is a fundamental property of binary floating-point arithmetic defined by IEEE 754.

Python is behaving correctly—it’s just exposing the limits of floating-point precision.entation.

3.2 Precision and Its Limits

Python floats provide approximately 15–17 significant decimal digits of precision. Beyond that, rounding errors become unavoidable.

Loss of Precision with Large Numbers

large = 1e20
print(large + 1)                 # 1e20
print(large + 1 == large)        # True

At this scale, the float simply cannot represent the difference between 1e20 and 1e20 + 1.

3.3 Understanding Float Internals with Built-in Methods

Python exposes several methods that help you understand how a float is stored internally.

Exact Fraction Representation

x = 1.5
print(x.as_integer_ratio())  # (3, 2)

Although 1.5 looks simple, Python stores it exactly as 3 / 2:

This method is extremely useful for:

  • Debugging precision issues

  • Verifying whether a value is exactly representable

  • Understanding rounding behavior

Hexadecimal Representation

print(x.hex())

This shows the exact IEEE 754 representation of the float in hexadecimal form.

3.4 Special Floating-Point Values: Infinity and NaN

IEEE 754 defines special values that Python fully supports.

Infinity

positive_inf = float('inf')
negative_inf = float('-inf')

print(positive_inf + 1)       # inf
print(1 / positive_inf)       # 0.0

NaN (Not a Number)

nan = float('nan')

4: Complex Numbers (complex)

Python includes native support for complex numbers, widely used in electrical engineering (AC circuits) and physics.

4.1 Structure

A complex number has a real part and an imaginary part. Python uses j to denote the imaginary unit (−1​).

# Creation
z1 = 3 + 4j
z2 = complex(2, -5)

# Attributes
print(f"Real part: {z1.real}")      # 3.0
print(f"Imaginary part: {z1.imag}") # 4.0

# Methods
print(f"Conjugate: {z1.conjugate()}") # 3-4j

4.2 The cmath Module

Standard math functions (like math.sqrt) will crash if given a negative number. For complex math, use cmath.

import cmath

# Square root of negative number
# math.sqrt(-1) -> ValueError
print(cmath.sqrt(-1))  # 1j

# Phase / Angle
print(cmath.phase(1 + 1j)) # 0.785... (Pi/4)

5: Arithmetic Operators and Division Semantics

Python supports all standard arithmetic operators, but with specific behaviors for division.

5.1 The Division Distinction

10 / 3   # 3.333...  (true division, always float)
10 // 3  # 3         (floor division)
10 % 3   # 1         (remainder)
  • / (True Division): Always returns a float.

  • // (Floor Division): Rounds down to the nearest whole integer.

  • % (Modulo): Returns the remainder.

5.2 The Negative Floor Division Trap

Floor division rounds down on the number line (to the left), not towards zero. This catches many developers off guard.

# Positive
print(10 // 3)   # 3

# Negative
# -3.33 rounds DOWN to -4
print(-10 // 3)  # -4

5.3 Operator Precedence (PEMDAS)

  1. Parentheses ()

  2. Exponentiation ** (Right-associative: 2**3**2 is 2(32))

  3. Unary Signs +x, -x

  4. Multiplication/Division *, /, //, %

  5. Addition/Subtraction +, -


6: Comparison Rules and Chaining

6.1 Chaining

Python allows mathematical chaining, which is cleaner and more readable than using logical and.

x = 15

# Traditional
if x > 10 and x < 20: # This is equivalent to (x > 10 and x < 20) but clearer and safer.
    print("InRange")

# Pythonic Chaining
if 10 < x < 20:
    print("InRange")

6.2 Mixed Type Comparison

You can compare int and float directly.

print(5 == 5.0)  # True
print(5 > 4.9)   # True

Note: complex numbers support equality checks, but cannot be ordered (< or >).


7: Rounding, Math Utilities, and Statistics

7.1 Banker's Rounding

The built-in round() function uses "Banker's Rounding" (Round Half to Even). This minimizes bias in large statistical datasets.

# Standard rounding
print(round(2.6)) # 3
print(round(2.4)) # 2

# Banker's rounding (.5 case)
print(round(2.5)) # 2 (Rounds to nearest EVEN number)
print(round(3.5)) # 4 (Rounds to nearest EVEN number)

7.2 Common Built-in Math Functions

abs(x)          # Absolute value (or magnitude for complex).
pow(x, y, m)    # Calculates (xy)(modz) very efficiently.
divmod(x, y)    # Return a tuple (x // y, x % y)
  • abs(x): Absolute value (or magnitude for complex).

  • pow(x, y, z): Calculates (xy)(modz) very efficiently.

  • divmod(x, y): Returns a tuple (x // y, x % y).

7.3 Math and Statistics Modules

Use math for scalar mathematics:

math.floor(x)
math.ceil(x)
math.log(x)
math.pi, math.e, math.tau

Use statistics for dataset analysis:

statistics.mean(data)
statistics.median(data)
statistics.stdev(data)

8: Type Conversion, Formatting, and Special Float Values

8.1 Explicit Casting (and data loss)

You can convert types explicitly, but be aware of data loss.

# float to int (Truncates decimal)
print(int(3.99))   # 3

# int to float
print(float(5))    # 5.0

# string to int
print(int("100"))  # 100

8.2 Parsing Errors

Python does not guess. If a string contains non-numeric characters, int() raises a ValueError.

# int("3.5")      # ValueError
# int("100abc")   # ValueError

# Correct way to parse "3.5" to int:
print(int(float("3.5"))) # 3

8.3 Formatting numbers with f-stringses a ValueError.

value = 1234.56789
print(f"Value with 2 decimal places: {value:,.2f}")
# Value with 2 decimal places: 1,234.57

Presenting numbers clearly is essential. F-strings allow precise formatting.

SpecifierDescriptionExample InputResult
:.2fFixed point (2 decimals)123.456123.46
:,.2fComma separator1234.51,234.50
:05dZero padding4200042
:+Always show sign42+42

9: Binary, Octal, and Hexadecimal Numbers

Python supports integer literals in multiple number bases, which is essential when working with low-level data, memory addresses, network protocols, and permissions.

9.1 Supported Bases and Prefixes

BasePrefixExampleDecimal Value
Binary0b0b10101042
Octal0o0o5242
Hexadecimal0x0x2A42

All of these create the same integer object internally.

9.2 Converting Numbers Between Bases

Python provides built-in helpers for converting integers to base-specific string representations:

# Converting to base-n strings
num = 255
print(f"Binary: {bin(num)}") # Binary: 0b11111111
print(f"Octal: {oct(num)}") # Octal: 0o377
print(f"Hexadecimal: {hex(num)}") # Hexadecimal: 0xff

print(int("0b11111111", 2)) # 255
print(int("0o377", 8)) # 255
print(int("0xff", 16)) # 255

10: Performance and Memory

Python numbers feel lightweight, but under the hood they are full Python objects. This design gives Python its flexibility—but it also introduces memory and performance trade-offs that are worth understanding.

10.1 Memory Footprint of Numeric Types

  • Every number in Python carries metadata such as type information and reference counts.

  • Integers (int):

    • Variable-sized objects

    • A small integer like 0 typically takes ~24–28 bytes

    • The memory footprint grows as the number of digits increases

    • This is the cost of Python’s arbitrary-precision arithmetic

  • Floating-point numbers (float):

    • Fixed-size objects (usually 24 bytes on 64-bit systems)

    • Stored as IEEE-754 double-precision values

    • Memory usage does not change with magnitude

  • This explains why Python integers can grow indefinitely, while floats cannot increase precision or range.

10.2 Speed and Optimization Notes

  • Python does not optimize numbers the same way low-level languages do. Understanding where costs appear helps you write faster code.

  • Integer arithmetic is:

    • Exact

    • Often faster for discrete operations

  • Floating-point arithmetic:

    • Hardware-accelerated

    • Subject to rounding and precision loss

  • The power operator (**) is highly optimized in CPython and should be preferred over manual multiplication loops.

  • Avoid unnecessary type casting in hot paths. Repeated conversions between int and float can silently slow down your code.

The Python Log: Learning in Public

Part 4 of 4

I’m sharing my journey from being Python-curious to Python-capable. This isn’t a textbook or a step-by-step course—it’s just me writing down the things I’m learning, the parts that confused me, and the insights that finally made things click.

Start from the beginning

01_Python Under the Hood

Understanding Python: Compilation, the PVM, and the Role of __pycache__ in Your Folders