The Honest Python: Deep Diving into CPython Internals

Arpit Bhayani

Arpit Bhayani

Apr 03, 2021 • 4 min read

Play

The Honest Python: Deep Diving into CPython Internals

Most software engineers interact with Python at a high level of abstraction, leveraging dynamic typing, ergonomic syntax, and an expansive standard library. However, treating Python as a black box conceals the engineering trade-offs, data structures, and runtime mechanics executing beneath the surface.

The Honest Python is an architectural and code-level exploration into CPython—the reference implementation of Python written in C. Rather than analyzing syntax or idioms, this series deconstructs how high-level abstractions translate into native C code, system memory representations, and CPU execution cycles.


Core Engineering Focus Areas

1. Arbitrary-Precision Arithmetic (Long Integers)

In systems languages like C or Go, numeric data types correspond to fixed hardware bit-widths (88, 1616, 3232, or 6464 bits) which can overflow when exceeding their bounds. Python integers, by contrast, feature arbitrary precision—they scale as large as available host memory allows.

+-------------------------------------------------------------+
|                        PyLongObject                         |
+-------------------+--------------------+--------------------+
|    PyObject_VAR_HEAD                   |     digit ob_digit | 
|  - ob_refcnt (reference count)         |   - array of digits| 
|  - ob_type   (pointer to &PyLong_Type) |     in base 2^30   |
|  - ob_size   (signed limb count)       |                    |
+-------------------+--------------------+--------------------+

Understanding how Python handles numeric operations requires dissecting PyLongObject (Include/cpython/longintrepr.h and Objects/longobject.c):

  • Digit Representation: How integers are segmented into arrays of digit “limbs” (typically stored in base 2302^{30} on 64-bit platforms).
  • Sign Encoding: Storing the sign directly within the object header (ob_size) rather than within the value payload itself.
  • Multiplication Algorithms: How small numbers use Karatsuba multiplication (O(n1.585)O(n^{1.585})) before falling back to more standard algorithmic baselines.

2. Core Container Implementations

Python’s built-in data types are heavily optimized to balance speed, memory overhead, and cache locality:

  • Lists (PyListObject): An over-allocated array of generic pointers (PyObject*). We investigate the geometric over-allocation strategy (Objects/listobject.c) that yields amortized O(1)O(1) appends while mitigating memory fragmentation.
  • Sets and Dictionaries (PyDictObject): Compact hash tables inspired by Raymond Hettinger’s design. The separation of a sparse hash index array and a dense insertion-ordered entry array minimizes wasted memory while preserving insertion order.
flowchart TD
    A[High-Level Python Operation] --> B[Bytecode Evaluation Loop: ceval.c]
    B --> C{Core Type Dispatch}
    C -->|list.append| D[PyList_Append: Dynamic Array Growth]
    C -->|dict[k] = v| E[lookdict: Compact Hash Table Probe]
    C -->|a + b| F[binary_op1: Arbitrary Precision Arithmetic]

3. Memory Management and Garbage Collection

Dynamic environments require robust memory reclamation without compromising throughput. CPython implements a two-tier model:

  1. Reference Counting (Py_INCREF / Py_DECREF):

    • Instantaneous deallocation when an object’s reference count drops to zero.
    • Deterministic and lightweight, but unable to detect or reclaim reference cycles (ABAA \to B \to A).
  2. Cyclic Garbage Collector (Tracing GC):

    • Handles cyclic references using a generational collection scheme (Generations 0, 1, and 2).
    • Modifies object headers to track incoming references, isolating and purging unreachable cyclic islands via Modules/gcmodule.c.
  3. Custom Small-Object Allocator (PyMalloc):

    • Bypasses raw system malloc() for allocations 512\le 512 bytes using an arena-pool-block hierarchical structure, reducing operating system context switches and heap fragmentation.

4. Modifying and Extending the CPython Runtime

Beyond auditing the codebase, true comprehension comes from altering the system. Exploring the compiler pipeline allows us to inject custom syntax, introduce non-standard operators, and alter bytecode generation:

flowchart LR
    Source[Python Code] -->|Grammar / Parser| CST[Concrete Syntax Tree]
    CST -->|AST Builder| AST[Abstract Syntax Tree]
    AST -->|Compiler| Bytecode[Python Bytecode .pyc]
    Bytecode -->|ceval.c Loop| Eval[Native CPU Execution]

By editing Grammar/python.gram, updating AST generation (Parser/), modifying the bytecode compiler (Python/compile.c), and hacking the virtual machine evaluation loop (Python/ceval.c), we can introduce new language primitives and observe firsthand how Python responds.


Key Takeaways

  • No Magic: Every high-level Python mechanic boils down to explicit C structures, pointer manipulations, and control flow.
  • Algorithmic Trade-offs: Dynamic ergonomics often require deliberate trade-offs in memory footprint, cache coherence, and pointer indirection.
  • First-Principles Mastery: Studying source code directly demystifies performance bottlenecks, memory leaks, and runtime anomalies.
Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses