04_Numbers in Python
Understanding How Python Represents and Works With Numbers

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: Includesintandbool(which is a subclass ofint).numbers.Real: Includesfloat.numbers.Complex: Includescomplex.
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)
Parentheses
()Exponentiation
**(Right-associative:2**3**2is 2(32))Unary Signs
+x, -xMultiplication/Division
*, /, //, %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.
| Specifier | Description | Example Input | Result |
:.2f | Fixed point (2 decimals) | 123.456 | 123.46 |
:,.2f | Comma separator | 1234.5 | 1,234.50 |
:05d | Zero padding | 42 | 00042 |
:+ | Always show sign | 42 | +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
| Base | Prefix | Example | Decimal Value |
| Binary | 0b | 0b101010 | 42 |
| Octal | 0o | 0o52 | 42 |
| Hexadecimal | 0x | 0x2A | 42 |
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
0typically takes ~24–28 bytesThe 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
intandfloatcan silently slow down your code.



