Primitive Data Types & Scalar Representation
Master CPython scalar internals: small integer singleton caching, arbitrary-precision PyLongObject digits, PEP 393 flexible string storage, and string interning.
Unlike languages that store scalar primitives directly in CPU registers or on the stack, Python treats all primitive values as heap-allocated objects. Even the simple integer 1 is an instance of PyLongObject with an object header.
To offset the performance overhead of full object instantiation, CPython employs aggressive optimization strategies: pre-allocated small integer caches, arbitrary-precision digit buffers, and flexible unicode representations with interning.
Integers from -5 to 256 are pre-allocated during interpreter boot and reused globally.
1. Small Integer Caching ([-5, 256])#
During interpreter startup, CPython initializes a contiguous array of 262 PyLongObject instances representing all integers in the inclusive range .
Whenever an expression produces an integer within this boundary, CPython bypasses memory allocation and returns a borrowed reference to the existing singleton:
12345678910# Caching range [-5, 256] a = 256 b = 256 print(f"256 is 256: {a is b}") # True: Identical pointer address # Outside caching range x = 257 y = 257 print(f"257 is 257: {x is y}") # False: Distinct heap allocations print(f"257 == 257: {x == y}") # True: Same mathematical value
2. Arbitrary-Precision Integers (PyLongObject)#
Python 3 removed the legacy distinction between 32-bit int and unbounded long. All integers in modern Python are represented by PyLongObject, capable of growing to arbitrary precision constrained only by system RAM.
Positional Number System Representation#
CPython stores large integers using a sign-magnitude base- positional numeral system:
Where:
- bits on 64-bit systems (or bits on 32-bit systems).
- Each digit is stored as an unsigned 32-bit integer (
uint32_t). - The sign of the integer is stored in the sign of
ob_size! A positive integer with 3 digits hasob_size = 3, while a negative integer with 3 digits hasob_size = -3. Zero hasob_size = 0.
3. PEP 393: Flexible String Representation#
Prior to Python 3.3, strings used a fixed 2-byte or 4-byte buffer per character, wasting enormous amounts of memory on ASCII text. PEP 393 introduced the Flexible String Representation (PyUnicodeObject), which dynamically adjusts character width based on the maximum Unicode code point in the text:
The total memory footprint for a string of length is:
Where the trailing byte holds a null terminator byte (\0), guaranteeing that C extensions can read the string buffer directly with zero conversion overhead.
12345678910111213import sys # Pure ASCII: 1 byte per character ascii_str = "hello world" print(f"ASCII bytes per char: {sys.getsizeof(ascii_str)} total bytes") # UCS-2 (Cyrillic / Greek): 2 bytes per character ucs2_str = "привет мир" print(f"UCS-2 bytes per char: {sys.getsizeof(ucs2_str)} total bytes") # UCS-4 (Emoji): 4 bytes per character for the entire string! ucs4_str = "hello 🐍" print(f"UCS-4 bytes per char: {sys.getsizeof(ucs4_str)} total bytes")
s[i] remains strictly , adding a single 4-byte emoji (like 🐍) to a 10,000-character ASCII text immediately inflates the entire string buffer to 4 bytes per character!4. String Interning Mechanics#
CPython maintains an internal interning dictionary mapping string contents to their canonical PyUnicodeObject instances.
When a string is interned, CPython checks if an identical string already exists in the interning table:
- If found: CPython decrements the candidate string refcount and returns a reference to the existing canonical string.
- If absent: The new string is added to the table.
All Python variable names, method names, and dictionary key literals are interned automatically during compilation. This allows dictionary and attribute lookups to compare memory addresses using single-instruction pointer equality () rather than scanning string contents byte-by-byte ().
5. Concept Verification#
Test your understanding of CPython scalar primitives:
How does CPython store integers exceeding standard 64-bit hardware register boundaries without causing arithmetic overflow?
Why does CPython intern string identifiers and dictionary keys?