Skip to main content

Lesson 1: What is CPython?

When you type python script.py into your terminal, something remarkable happens behind the scenes. Your code passes through several transformation layers before your computer actually runs it. Understanding this process—and understanding that "Python" isn't just one thing—is foundational to working effectively with the language, especially when you need to optimize multi-agent AI systems or deploy Python code in constrained environments.

In this lesson, you'll discover what CPython actually is, how it differs from other Python implementations, and how Python code becomes executable instructions. This foundation is essential for Lesson 2, where we'll explore the Global Interpreter Lock (GIL) and understand why threading behaves differently in CPython than in alternative implementations.

Section 1: What is CPython?

When you download Python from python.org and install it on your computer, you're installing CPython. This is the reference implementation of Python—the official, authoritative version of the Python language.

Let's break this down carefully. "Reference implementation" means:

  1. Official Standard: CPython is the canonical definition of what "Python" means. When the Python Enhancement Proposal (PEP) process creates new features, CPython is the first to implement them. If something works in CPython, it's "officially Python."

  2. Maintained by Python Software Foundation: The core Python developers (led by Guido van Rossum until 2019, now governed by a steering council) maintain CPython. It's the authoritative decision-maker for language behavior.

  3. Written in C: Here's a crucial detail that will matter for everything we discuss about performance and threading—CPython isn't written in Python. It's written in C, a lower-level programming language. This design choice has profound implications for how memory is managed and how Python handles concurrent execution.

💬 AI Colearning Prompt

Before we continue, let's explore why this architecture matters:

"Why is CPython written in C instead of Python? What are the advantages of implementing Python in C? What would happen if Python was implemented entirely in Python instead?"

You'll discover that implementing a language is a bootstrapping problem—you need a lower-level language to build the higher-level one. You'll also learn about performance and integration: C allows CPython to integrate with operating systems, hardware, and performance-critical code seamlessly.

Section 2: The Python Execution Pipeline

To understand CPython, you need to understand how code becomes execution. There are three main stages:

Stage 1: Source Code (Your Python Files)

You write Python code in .py files:

def greet(name: str) -> str:
"""Return a personalized greeting."""
return f"Hello, {name}!"

message = greet("Alice")
print(message)

This is human-readable Python. It follows Python's syntax rules and is meant for developers to understand and maintain.

Stage 2: Bytecode Compilation (The Recipe Card)

CPython doesn't execute your source code directly. Instead, it compiles your code to bytecode—a lower-level, machine-independent representation that's more efficient to execute.

Think of bytecode as a recipe card. Your source code is detailed instructions; bytecode is the essential steps condensed. When you run a Python script, CPython:

  1. Parses your source code
  2. Compiles it to bytecode
  3. Writes the bytecode to a .pyc file (usually in __pycache__/)
  4. Caches it for next time

You've probably seen those __pycache__ folders in your projects. Those contain the bytecode:

my_project/
__pycache__/
greet.cpython-314.pyc # Bytecode for greet.py
greet.py

The .cpython-314.pyc filename tells you:

  • cpython: This bytecode is for CPython
  • 314: This is Python 3.14
  • .pyc: Compiled Python code

🎓 Instructor Commentary

In AI-native development, you don't memorize what bytecode looks like or how to read it. What matters is understanding when bytecode affects your workflow:

  • Debugging: Bytecode caching means changes are sometimes compiled; understanding this prevents "why isn't my change working?" confusion
  • Deployment: Bytecode is platform-independent, so you can deploy .pyc files instead of source (for secrecy or performance)
  • Performance: The compilation step happens once and is cached, making repeated runs faster

Syntax is cheap—understanding this architectural layer is gold.

Stage 3: Interpreter Execution (Following the Recipe)

The CPython interpreter reads the bytecode and executes it. The interpreter is the C program that:

  1. Reads bytecode instructions one by one
  2. Manages objects in memory
  3. Executes operations (add numbers, call functions, access variables)
  4. Returns results

Here's where it gets crucial: the CPython interpreter is single-threaded at the bytecode level. This is the Global Interpreter Lock (GIL) in action, which we'll explore in Lesson 2. For now, understand that the interpreter processes bytecode sequentially—even if you write multi-threaded code, the bytecode-level execution is single-threaded.

🚀 CoLearning Challenge

Ask your AI Companion:

"Walk me through what happens when I run python script.py. Create a diagram showing: source code → compilation → bytecode → interpreter execution. Explain what CPython is doing at each stage, and identify where the GIL might become relevant."

Expected Outcome: You'll develop a mental model of Python execution that explains why some optimizations work better than others, and why threading behaves differently in CPython.

Section 3: Memory Management Deep Dive

CPython manages memory using two mechanisms: reference counting and garbage collection.

Reference Counting: The Primary Mechanism

Every object in CPython has a reference count—a number tracking how many parts of your code are currently using that object.

x: list[int] = [1, 2, 3]  # refcount([1,2,3]) = 1, x holds the reference
y = x # refcount([1,2,3]) = 2, now both x and y reference it
z = x # refcount([1,2,3]) = 3, now x, y, z all reference it

del y # refcount([1,2,3]) = 2, y released its reference
del z # refcount([1,2,3]) = 1, z released its reference
del x # refcount([1,2,3]) = 0, OBJECT IS FREED!

When the reference count drops to zero, CPython immediately frees that object's memory. This is deterministic—you know exactly when memory is released.

Why is this important? Reference counting is fast and simple. It doesn't require pausing your program to scan for unused objects (which garbage collection does). For real-time systems and AI inference workloads, this predictability is valuable.

✨ Teaching Tip

Use Claude Code or Gemini CLI to explore this interactively:

"Show me Python code that demonstrates reference counting. Create objects, increase their refcount, then show what happens when refcount drops to zero. Explain what 'del' does and why it matters for memory."

Garbage Collection: Handling Cycles

Reference counting alone has a weakness: circular references. When objects refer to each other in a loop, reference counts never reach zero—even though nothing outside the cycle refers to them.

class Node:
def __init__(self) -> None:
self.next: Node | None = None

a = Node()
b = Node()
a.next = b # a → b
b.next = a # b → a (CYCLE!)

del a
del b
# Reference counts are still 1 each (they reference each other)
# Without garbage collection, memory would leak!

CPython's garbage collector runs periodically to find and free these unreachable objects. It works by:

  1. Marking all accessible objects starting from known root references
  2. Sweeping unreachable objects and freeing them

The garbage collector typically runs when certain thresholds are reached (e.g., 1,000 objects created since last collection). You can control this with the gc module:

import gc

# Check GC stats
print(gc.get_stats())

# Force immediate garbage collection
gc.collect()

# Disable GC for performance-critical code
gc.disable()
# ... performance-critical section ...
gc.enable()

💬 AI Colearning Prompt

"Explain the difference between reference counting and garbage collection. Why does CPython use both? What are the advantages and disadvantages of each approach?"

This exploration will help you understand why CPython's design leads to the GIL. Different designs (like PyPy's moving garbage collector) can avoid the GIL by using different memory management.

Section 4: Alternative Implementations

"Python" the language is separate from "CPython" the implementation. Several other implementations exist, each with different tradeoffs:

PyPy: Speed Through JIT Compilation

PyPy is a Python implementation written largely in Python itself. It uses Just-In-Time (JIT) compilation—analyzing code at runtime and compiling hot paths to machine code.

Source Code

Interpreter (faster feedback)

JIT detects "hot" code

Compile to machine code

Execute machine code (MUCH faster)

Tradeoff: PyPy's startup time is slower (compilation overhead), but repeated code runs much faster—sometimes 2-10x faster than CPython on compute-intensive workloads.

Use when: Your code has computational bottlenecks (matrix math, data processing, simulations). Avoid when you need immediate startup (CLI tools, serverless functions).

Jython: Python on the Java Virtual Machine

Jython runs Python code on the Java Virtual Machine (JVM), letting you:

  • Use Java libraries from Python
  • Deploy Python code in Java environments
  • Leverage JVM optimizations and threading

Tradeoff: Jython is slower than CPython for most tasks, but gains Java ecosystem access and better native threading.

Use when: You need to integrate with Java systems or deploy in Java-only environments.

IronPython: Python on the .NET Platform

Similar to Jython but for Microsoft's .NET platform. Useful for integrating Python into Windows/C# environments.

Use when: You're in a .NET ecosystem and need Python scripting capabilities.

MicroPython: Python for Embedded Systems

MicroPython is a minimal Python implementation for microcontrollers (Arduino-like devices), IoT systems, and embedded devices.

# MicroPython on a microcontroller
import machine

led = machine.Pin(2, machine.Pin.OUT)
led.on() # Turn on LED
led.off() # Turn off LED

Tradeoff: MicroPython omits some Python features to fit in kilobytes of RAM. You get Python-like syntax for embedded systems.

Use when: Writing code for IoT devices, microcontrollers, or resource-constrained environments.

Comparison Table

ImplementationSpeedMemoryPython CompatibilityBest ForKey Tech
CPythonBaselineStandard100%General purpose, productionReference counting + GC
PyPy2-10x fasterMore~95%Compute-heavy workloadsJIT compilation
JythonSlowerJVM~95%Java integrationRuns on JVM
IronPythonSlowerCLR~95%.NET integrationRuns on .NET
MicroPythonFast (tiny)Tiny~70%Embedded/IoTMinimal footprint

Section 5: How to Detect Your Implementation

In production systems, you often need to know which Python implementation is running. The platform module provides this information:

Specification Reference: Using Python's platform module for implementation detection

AI Prompt Used: "Create a Python function using platform.python_implementation() that detects the Python implementation and returns helpful information about it."

Generated Code:

import platform
import sys
from typing import dict

def detect_python_implementation() -> dict[str, str]:
"""
Detect and return information about the Python implementation.

Returns:
Dictionary containing implementation info (name, version, etc.)
"""
impl_name = platform.python_implementation()
py_version = platform.python_version()
py_compiler = platform.python_compiler()

return {
"implementation": impl_name,
"version": py_version,
"compiler": py_compiler,
"executable": sys.executable,
"prefix": sys.prefix,
}

# Run it
info = detect_python_implementation()
for key, value in info.items():
print(f"{key:15} : {value}")

Example Output on CPython:

implementation  : CPython
version : 3.14.0
compiler : GCC 11.2.0
executable : /usr/bin/python3.14
prefix : /usr

On PyPy:

implementation  : PyPy
version : 7.3.12
compiler : GCC 11.2.0
executable : /usr/bin/pypy3
prefix : /home/user/.pyenv/versions/pypy3.9-7.3.12

Validation Steps:

  1. Run on your local CPython installation → should print "CPython"
  2. If you have PyPy installed, run same script with pypy3 script.py → should print "PyPy"
  3. Confirm sys.executable path differs between implementations
  4. Use this in CI/CD to handle implementation-specific behavior

🚀 CoLearning Challenge

Ask your AI Companion:

"Generate a Python script that detects the implementation and conditionally enables features based on which one is running. For example: 'If PyPy, use speed optimization X. If CPython, use feature Y.' Explain the conditional logic."

Expected Outcome: You'll understand how to write implementation-aware code and recognize when different implementations require different approaches.

This is especially important in multi-agent AI systems where you might deploy on PyPy for performance but develop on CPython for compatibility.

Why This Matters in Production

Consider a data processing pipeline for AI workloads:

import platform

# AI-native development: detect and optimize
if platform.python_implementation() == "PyPy":
# PyPy excels at computation
use_expensive_computation = True
process_batch_size = 10000 # Larger batches
elif platform.python_implementation() == "CPython":
# CPython with C extensions (numpy) is often better
use_expensive_computation = False
process_batch_size = 1000 # Smaller batches
else:
# Unknown implementation: conservative defaults
use_expensive_computation = False
process_batch_size = 500

This code adapts to its environment—a pattern you'll use repeatedly in production systems.

Why CPython's Design Matters for What's Coming

Understanding CPython sets up the next lesson perfectly. The key insights:

  1. Reference counting is CPython's primary memory strategy
  2. Single-threaded bytecode execution is a consequence of reference counting
  3. The Global Interpreter Lock (GIL) protects reference counters in threaded code

These three facts explain GIL behavior in Lesson 2, threading limitations in Lesson 3, and why async programming exists as an alternative in Lesson 4.

Alternative implementations like PyPy escape these constraints by using different memory management strategies. This is the architectural foundation for performance optimization in multi-agent AI systems—you'll choose implementations based on your workload characteristics.


Try With AI

Use Claude Code or Gemini CLI (or your AI companion tool if already set up). Work through these prompts in order, allowing 10-15 minutes total.

Prompt 1: Remember – Define CPython

"In your own words, explain what CPython is and why it's called the 'reference implementation.' Then ask your AI: 'Is my explanation accurate? What am I missing?' Refine your understanding based on feedback."

What you'll learn: Validate your understanding of CPython's role in the Python ecosystem and its relationship to alternative implementations.

Expected time: 2-3 minutes


Prompt 2: Understand – Execution Pipeline

"Ask your AI: 'Walk me through the Python execution pipeline step-by-step. What happens when I type python script.py? What is the CPython interpreter doing at each stage?' Then ask: 'Where does bytecode fit in? Why can't CPython just execute source code directly?'"

What you'll learn: Develop a mental model of how source code becomes execution, including bytecode compilation and the role of the CPython interpreter.

Expected time: 3-4 minutes


Prompt 3: Apply – Implementation Detection

"Tell your AI: 'Create a Python script that detects what Python implementation I'm using and prints helpful diagnostic information. Include: implementation name, version, executable path. Make it useful for CI/CD systems.' Then run the script and verify it works."

What you'll learn: Write production-ready code using the platform module; understand why implementation detection matters for deployment systems.

Expected time: 3-4 minutes


Prompt 4: Analyze – CPython Design and GIL Connection

"Ask your AI: 'How does CPython's design (reference counting + C API) affect the GIL (which we'll learn about next)? What's the connection between memory management and threading constraints?' Then speculate: 'Why might alternative implementations like PyPy avoid the GIL? How does different memory management lead to different threading behavior?'"

What you'll learn: Make the cognitive leap from CPython internals to GIL consequences; preview why Lesson 2 focuses on threading constraints; understand how design choices cascade through a system.

Expected time: 3-5 minutes


Safety & Ethics Note: When exploring implementation differences, you're learning about computational tradeoffs, not discovering security vulnerabilities. Understanding why CPython makes certain choices builds respect for the engineering involved. Different implementations exist because they solve different problems—there's no "wrong" choice, just "right for this use case."

If you're using a CLI tool (Claude Code or Gemini CLI), here are the command equivalents:

# Run prompt 3 directly
claude code "Create a Python script that detects what Python implementation I'm using..."

# Or use a plain-text chat equivalent

If you've already set up an AI companion tool from previous chapters, use it instead of the web interface. All these prompts work with any major AI language model.