Sitemap

How CPython Works Under the Hood

5 min readMar 10, 2026

--

A Deep Dive into the Python Execution Engine (For Backend & DevOps Engineers)

Why You Should Care

When you run:

print("Hello, World")

You are not “running Python.”

You are running CPython — the reference implementation of Python written in C.

Understanding how CPython works internally helps you:

  • Debug performance bottlenecks
  • Optimize memory usage
  • Design better concurrency models
  • Make smarter scaling decisions in Kubernetes
  • Evaluate runtime upgrades confidently

This post breaks down the entire execution lifecycle — from source code to machine instructions — with diagrams and production relevance.

🧠 What Is CPython?

CPython is the default and most widely used implementation of Python.

It is:

  • Written in C
  • Interpreted (bytecode-based VM)
  • Reference-counted memory managed
  • GIL-protected (in standard builds)

Other implementations exist (PyPy, Jython), but when you type python, you're almost certainly running CPython.

🔄 High-Level Execution Flow

Here is the simplified lifecycle when you run a Python file:

Press enter or click to view image in full size

Step-by-step:

  1. Source Code (.py)
  2. Lexer & Parser
  3. AST (Abstract Syntax Tree)
  4. Bytecode Compilation
  5. Python Virtual Machine (PVM)
  6. Execution via Evaluation Loop
  7. Memory Management (Ref Counting + GC)

Let’s go deeper into each stage.

1️⃣ Lexing & Parsing

When CPython reads your code:

x = 10 + 20

It first tokenizes it:

  • NAME (x)
  • EQUALS
  • NUMBER (10)
  • PLUS
  • NUMBER (20)

Then builds an AST (Abstract Syntax Tree).

Inspecting the AST

import ast
code = "x = 10 + 20"
tree = ast.parse(code)
print(ast.dump(tree, indent=4))

This produces a tree-like structure representing program logic.

Why This Matters

  • Linters operate on AST
  • Security scanners analyze AST
  • Code transformation tools use AST
  • Static analyzers depend on this layer

For DevOps pipelines, AST-based tools power:

  • SAST scanning
  • Policy enforcement
  • Code quality gates

2️⃣ Compilation to Bytecode

The AST is compiled into bytecode — instructions for the Python Virtual Machine.

You can inspect bytecode using dis.

import dis

def add(a, b):
return a + b
dis.dis(add)

Example output:

LOAD_FAST 0 (a)
LOAD_FAST 1 (b)
BINARY_OP +
RETURN_VALUE

This is not machine code.

It is CPython bytecode, executed by the evaluation loop.

3️⃣ The Python Virtual Machine (PVM)

The PVM is a stack-based virtual machine.

Unlike register-based VMs (like JVM), CPython:

  • Pushes operands to stack
  • Performs operations
  • Pops results

Example:

For a + b:

  1. Push a
  2. Push b
  3. Execute BINARY_OP
  4. Push result

4️⃣ The Evaluation Loop (The Heart of CPython)

The most critical file in CPython source:

Python/ceval.c

This contains the evaluation loop.

Simplified version:

while (true) {
opcode = next_instruction();
switch (opcode) {
case LOAD_FAST:
...
case BINARY_OP:
...
case RETURN_VALUE:
...
}
}

This loop:

  • Fetches bytecode
  • Decodes opcode
  • Executes corresponding C logic
  • Moves to next instruction

🔥 Modern Optimization: Adaptive Interpreter

In modern Python versions:

Instead of generic opcodes:

LOAD_ATTR

CPython dynamically specializes to:

LOAD_ATTR_INSTANCE_VALUE

This reduces:

  • Dictionary lookups
  • Attribute resolution overhead
  • Cache misses

This makes CPython faster without full JIT compilation.

5️⃣ Frame Objects & Call Stack

Each function call creates a frame object.

A frame contains:

  • Local variables
  • Reference to code object
  • Stack
  • Instruction pointer
  • Link to previous frame

You can inspect frames:

import inspect

def demo():
frame = inspect.currentframe()
print(frame.f_locals)
demo()

Production Impact

Heavy function nesting:

  • Increases frame creation
  • Increases memory churn
  • Affects async-heavy apps

Modern Python reduces unnecessary frame allocation.

6️⃣ Object Model Internals

Everything in Python is an object.

Get DevOpsByte’s stories in your inbox

Join Medium for free to get updates from this writer.

Internally:

Every object begins with:

typedef struct {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;

Every Python object contains:

  • Reference count
  • Type pointer

This is why:

a = 10
b = a

Does not copy the integer — it increases refcount.

7️⃣ Memory Management

CPython uses:

1️⃣ Reference Counting

Every object tracks how many references point to it.

When count reaches zero → object is freed.

2️⃣ Generational Garbage Collector

Handles cyclic references:

a = []
a.append(a)

Reference counting alone cannot free this.

GC periodically scans generations.

Memory Allocation (pymalloc)

CPython uses a custom allocator:

  • Optimized for small objects
  • Reduces system calls
  • Minimizes fragmentation

DevOps Insight

In containers:

  • Fragmentation increases RSS
  • High RSS leads to OOMKill
  • Memory profiling becomes critical

8️⃣ The Global Interpreter Lock (GIL)

CPython uses a Global Interpreter Lock.

Only one thread executes Python bytecode at a time.

Why?

Because:

  • Reference counting is not thread-safe
  • Removing GIL requires atomic operations

Effect:

  • CPU-bound tasks don’t scale with threads
  • I/O-bound tasks scale well

DevOps Scaling Strategy:

9️⃣ Import System Internals

When you run:

import math

CPython:

  1. Checks sys.modules
  2. Searches sys.meta_path
  3. Loads module
  4. Compiles to bytecode
  5. Caches .pyc

The import system is extensible.

This is how:

  • Plugin systems work
  • Dynamic loaders operate
  • Zip imports function

🔟 Bytecode Caching (.pyc Files)

Python creates .pyc files in __pycache__.

Benefits:

  • Faster startup
  • Reduced re-compilation

In Docker:

  • Pre-compiling reduces cold start time
  • But increases image size slightly

1️⃣1️⃣ Async Internals

Async functions:

async def fetch():
...

Are compiled into:

  • Coroutine objects
  • Scheduled on event loop
  • Managed via state machine

Underneath:

  • Await points save frame state
  • Resume execution later

This is why async avoids OS threads.

🐳 How All This Matters in Kubernetes

Understanding CPython internals helps with:

1️⃣ Pod Sizing

  • Memory model → define correct limits
  • Avoid GC pauses
  • Prevent OOM

2️⃣ Horizontal Scaling

  • GIL limits CPU-bound scaling
  • Multiple workers required

3️⃣ Cold Start Optimization

  • Import cost
  • Bytecode caching
  • Startup frame creation

🔬 Real-World Example

Consider:

FastAPI service

  • 8 cores
  • 2GB RAM
  • High JSON serialization

Knowing:

  • Frame cost
  • Exception path optimization
  • Attribute specialization

Helps tune:

  • Worker count
  • Thread vs process
  • Memory limits

🎯 Key Takeaways

CPython:

  • Is a stack-based virtual machine
  • Executes bytecode via evaluation loop
  • Uses reference counting + generational GC
  • Is protected by GIL (standard build)
  • Dynamically specializes bytecode
  • Uses frame objects for call stack
  • Optimizes memory via pymalloc

Understanding this lets you:

  • Debug performance scientifically
  • Optimize architecture decisions
  • Avoid cargo-cult scaling
  • Write production-grade Python systems

--

--

DevOpsByte
DevOpsByte

Written by DevOpsByte

Insights on DevOps Engg, Cloud platforms (AWS, Azure), Kubernetes, Docker, CI/CD pipelines, Terraform, Ansible, monitoring, and backend development.