Видео с мероприятий {speach!
CPython's early exposure of raw C struct slots and static type pointers locked external libraries into legacy mechanics, permanently barring Global Interpreter Lock removal and leaving Python slower and less dynamic than Ruby.
AI-assisted summary of How Python was Shaped by leaky Internals, Armin Ronacher, Flask framework — original source. Check the original for full context.
Source published:
Key ideas
- CPython's arithmetic dispatch does not simply map to special dunder methods like __add__; instead, it relies on low-level C struct function pointers (slots) and fast-path type checks spanning hundreds of lines of C code. (5:36)
- Exposing raw interpreter internals—including sys._getframe(), sys.modules, and reference-counting C APIs—has locked Python into backward compatibility constraints that prevent removing the Global Interpreter Lock (GIL) and supporting multi-version module imports. (43:19)
- CPython's built-in primitive types reside at static global C memory addresses, preventing embedders like Nginx from safely running multiple isolated Python interpreter instances across threads in a single process. (39:53)
- Python's commitment to O(1) character indexing forced memory-wasteful fixed-width Unicode implementations (UCS-2 and UCS-4), unlike modern languages like Go and Rust that embrace variable-length UTF-8 without random character indexing. (30:03)
Detailed notes
## Background This talk was delivered by Armin Ronacher, author of the Flask framework, and published on YouTube by the channel 'Видео с мероприятий {speach!' on July 13, 2016. No description or show notes were provided with the original upload.
## TL;DR Armin Ronacher examines how CPython's low-level C virtual machine implementation has inadvertently dictated Python's language semantics and restricted its evolution. He deconstructs the common misconception that basic operations like addition simply resolve to dunder method lookups like `__add__`, revealing instead an intricate maze of internal C slots, fast-path type checks, and sequence concatenation fallbacks. Ronacher highlights how leaky internals—ranging from fixed-width Unicode indexing and static global type structs to exposed stack frames via `sys._getframe()` and mutable global caches in `sys.modules`—have empowered debugging tools while permanently locking the ecosystem into decisions that obstruct multithreading, versioned imports, and removing the Global Interpreter Lock (GIL). Ultimately, he argues that Python sacrificed architectural purity and cache-friendly performance for ad-hoc optimizations, offering lessons for future runtime designers on keeping internal APIs minimal.
## Atmosphere & style The presentation is candid, self-deprecating, and unapologetically technical, balancing deep low-level mechanics with dry humor about Python's historical idiosyncrasies. Ronacher speaks not as an ideological evangelist, but as a battle-hardened open-source maintainer sharing a mix of pragmatic admiration and lingering frustration over the interpreter's architectural traps. Watching the talk feels like peering under the hood of a beloved, familiar engine alongside a master mechanic who openly confesses where the bolts are stripped, creating an engaging, conversational atmosphere that invites active curiosity from the audience.
## Bulletin - [05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) CPython's arithmetic dispatch does not simply map to special dunder methods like __add__; instead, it relies on low-level C struct function pointers (slots) and fast-path type checks spanning hundreds of lines of C code. - [43:19](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2599s) Exposing raw interpreter internals—including sys._getframe(), sys.modules, and reference-counting C APIs—has locked Python into backward compatibility constraints that prevent removing the Global Interpreter Lock (GIL) and supporting multi-version module imports. - [39:53](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2393s) CPython's built-in primitive types reside at static global C memory addresses, preventing embedders like Nginx from safely running multiple isolated Python interpreter instances across threads in a single process. - [30:03](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1803s) Python's commitment to O(1) character indexing forced memory-wasteful fixed-width Unicode implementations (UCS-2 and UCS-4), unlike modern languages like Go and Rust that embrace variable-length UTF-8 without random character indexing.
## Key takeaways - [05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) Teaching that binary addition translates directly to calling a.__add__(b) is inaccurate; CPython executes BINARY_ADD via fast-path checks and C-level type slots rather than attribute lookups. - [11:54](https://www.youtube.com/watch?v=qCGofLIzX6g&t=714s) CPython operates two parallel systems—C struct function pointers (slots) and Python class dictionaries—and expends significant runtime machinery attempting to synchronize them. - [10:39](https://www.youtube.com/watch?v=qCGofLIzX6g&t=639s) Binary operator evaluation remains largely unspecified and emergent, leading alternative implementations like PyPy to occasionally disagree with CPython's exact behavior in edge cases. - [17:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1072s) Internal artifacts like slot_wrapper and method_descriptor leak into Python userland, producing observable behavioral discrepancies between native types and user-defined classes that production code now relies upon. - [21:45](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1305s) In older Python versions, built-in C types like compiled regex objects bypassed standard dunder protocols entirely, preventing pure Python implementations of built-ins like repr(). - [23:51](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1431s) The evolution from old-style to new-style classes broke dynamic transparent proxying via __getattr__ because special method lookup was moved directly to class-level slot inspection. - [25:27](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1527s) Early Unicode adoption led Python to guarantee O(1) random character indexing via fixed-width UCS-2 and UCS-4 arrays, causing massive memory bloat compared to UTF-8 representations. - [32:46](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1966s) While considered bad language design, exposing sys._getframe() became the foundation for critical ecosystem tooling, including Sentry, Flask's interactive debugger, and Twisted Manhole. - [37:35](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2255s) Relying on sys.modules as a globally exposed flat dictionary makes side-by-side loading of multiple library versions impossible without breaking backward compatibility for tools like pickle. - [39:53](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2393s) Built-in types like int use static global memory pointers for rapid pointer equality checks, which prevents embedding multiple isolated interpreters across threads within a single operating system process. - [43:19](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2599s) The Global Interpreter Lock cannot be easily removed because foundational interpreter structures, shared static types, and third-party C extension reference counting depend on existing thread-unsafe guarantees. - [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) Keeping the public interpreter API minimal—as JavaScript engines do—prevents ecosystem lock-in and allows runtimes to evolve and optimize aggressively. - [47:25](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2845s) CPython's complex special-casing and bloated instruction footprint cause CPU instruction cache misses and branch mispredictions, leaving Python slower per opcode than fully dynamic languages like Ruby.
## Detailed walkthrough ### [00:06](https://www.youtube.com/watch?v=qCGofLIzX6g&t=6s) Introduction and Armin Ronacher's Background
Armin Ronacher opens the talk by explaining that the subject is Python inside the virtual machine and how internal implementation choices end up shaping Python as a language. Because internal virtual machine mechanics can easily become dry and tedious, he encourages the audience to interrupt and ask questions throughout to keep things lively. He points out a curious tension: while developers broadly enjoy writing Python, the interpreter itself is remarkably complex and restrictive under the hood, simultaneously enabling certain workflows while severely limiting others.
```mermaid flowchart TD accTitle: Armin Ronacher Open Source Ecosystem accDescr: Diagram illustrating Armin Ronacher's major projects and ecosystem contributions. subgraph SentryEcosystem["Sentry Stack"] S["Sentry (Error Reporting)"] D["Django Framework Base"] S --> D end subgraph WebAndTooling["Web Frameworks & Libraries"] F["Flask Framework"] L["Lector (Static CMS)"] W["Werkzeug"] J["Jinja2"] M["MarkupSafe"] end ```
Ronacher sketches his own history with the runtime. His first contact with Python was version 2.0, though he actively started developing on Python 2.2. Driven by curiosity about how the language and its interpreter evolved, he recently compiled Python 1.3 on a modern computer—a surprisingly smooth process that yielded a functional binary and revealed how much legacy code has persisted untouched across decades. Despite holding commit access to CPython for "inexplicable reasons," he clarifies that he has rarely contributed directly to the core runtime; his perspective comes instead from building large systems and popular libraries, including his day job on Sentry (a Django-based error reporting tool), authoring the Flask web framework, creating the static CMS Lektor, and building core libraries like Werkzeug, Jinja2, and MarkupSafe.
- First encountered Python at version 2.0; began writing active code around version 2.2. - Successfully built and ran Python 1.3 on modern machines to inspect original interpreter behavior. - Core author/maintainer of Flask, Lektor, Werkzeug, Jinja, and MarkupSafe. - Primary day job: engineering at Sentry, a Python and Django-based error-tracking platform.
---
[02:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=172s) Dynamic Languages, Optimization, and the Constraints of CPython
Surveying the landscape of programming languages on an axis ranging from strictly static to highly dynamic, Ronacher contrasts Python with Ruby. The Ruby community often highlights its extreme dynamism, such as monkey-patching core primitives—for instance, redefining arithmetic on the `Float` class to turn addition into subtraction. Python nominally forbids patching core built-in types such as overriding `float.__add__`. Yet Ronacher argues that Python is actually far more dynamic than Ruby in ways that make it significantly more difficult to optimize.
Because tutorials and documentation describe an idealized model of how Python executes, developers are often blind to what the virtual machine is really doing. When internal realities conflict with expectations, changing the runtime becomes almost impossible because production codebases inadvertently depend on quirky implementation details. These historical guarantees constrain major architectural redesigns:
> “Certain things that people want to have for a long time is getting rid of the global interpreter lock and that turns out to be a hard and also very much relevant to how the interpreter was designed initially and what guarantees it gives…”
These structural restrictions prevent the runtime from: - Removing the Global Interpreter Lock (GIL) to achieve true multicore parallelism. - Loading multiple distinct versions of the same shared library into the same process simultaneously. - Running multiple isolated sub-interpreters side-by-side within a single executable process without shared state leaks. - Modernizing core type system internals without breaking backwards compatibility.
Ronacher adds that Python 3 compounded this friction by layering substantial architectural complexity on top of an already intricate core, sometimes with insufficient design peer review.
---
[05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) The "Big Lie" of `__add__` and the Anatomy of Addition
Ronacher displays a simple Python snippet to illustrate the gap between developer perception and interpreter reality:
```python magic = 42
def add_magic(a): return a + magic ```
Standard tutorials routinely teach that executing `a + magic` evaluates conceptually to invoking `a.__add__(magic)`. Ronacher flatly rejects this: “A lot of tutorials on the internet will tell you that this is actually equivalent to this, which it's not really… in fact it is totally not that.”
Instead, the source code compiles down to CPython bytecode containing the `BINARY_ADD` opcode. Execution shifts inside `ceval.c`, an enormous file of several thousand lines structured around a massive evaluation loop and switch statement handling every single opcode.
```mermaid flowchart TD accTitle: CPython BINARY_ADD Execution Flow accDescr: Step by step internal path for evaluating BINARY_ADD down to C slots. A["Python code: a + magic"] --> B["Bytecode: BINARY_ADD"] B --> C["ceval.c: main opcode evaluation loop"] C --> D{"Exact type checks on stack args"} D -->|"Exact int + int"| E["Fast-path integer addition"] D -->|"Exact str + str"| F["Fast-path string concatenation"] D -->|"Fallback / Generic"| G["PyNumber_Add(v, w)"] G --> H["Inspect C struct slot: tp_as_number->nb_add"] H --> I["Sequence concatenation check or binary operation"] ```
When `BINARY_ADD` executes, it pops two operand arguments (`v` and `w`) from the value stack and walks through an extensive sequence of conditionals: - It checks whether both arguments are strictly, exactly `int` (excluding subclasses). - It checks whether both arguments are strictly, exactly `str`. - If neither fast-path matches, control drops into `PyNumber_Add(v, w)`.
Despite its name, `PyNumber_Add` is misleadingly labeled; Ronacher points out that it functions as a general `PyObject_Add` because it also encapsulates sequence concatenation logic.
---
[08:32](https://www.youtube.com/watch?v=qCGofLIzX6g&t=512s) C Slots vs. Dunder Methods in CPython
Ronacher clarifies a fundamental ambiguity in Python terminology regarding "slots." Most developers associate the word purely with the class-level `__slots__` attribute, which eliminates an instance's underlying `__dict__` to conserve memory. However, the runtime relies on a second, distinct kind of slot: internal C-level struct function pointers embedded directly within `PyTypeObject`.
Every Python object contains an `ob_type` pointer referencing its type descriptor. Within this type structure lies `tp_as_number`, a pointer to a `PyNumberMethods` struct containing function pointers such as `nb_add`. Instead of looking up the string `"__add__"` inside an attribute dictionary, the C execution engine inspects the `nb_add` function pointer slot of both operands:
- The runtime checks `v->ob_type->tp_as_number->nb_add`. - It evaluates the relationship between the types of `v` and `w` to determine method precedence and handle reverse operations. - The C implementation required to coordinate and dispatch this single addition operation amounts to roughly 400 lines of low-level C code.
Ronacher concludes that teaching developers that arithmetic translates directly to `__add__` lookup is "the big lie." The runtime bypasses attribute dictionary lookups in favor of these fixed C function pointer slots, creating an execution reality radically detached from standard object model explanations.
[10:39](https://www.youtube.com/watch?v=qCGofLIzX6g&t=639s) The Mystery of Adding Two Objects
Reasoning about fundamental operations inside the Python interpreter turns out to be surprisingly frustrating. In an ideal world, taking object `A` and object `B` and applying addition, subtraction, or multiplication would trigger a simple, deterministic sequence of steps. JavaScript, for instance, introduced clear standards specifying exactly how two objects are evaluated during addition. In Python, however, determining what actually happens is remarkably difficult. Especially in Python 2, the outcome depends on internal object flags and intricate, specific type relationships between the two operands. As Armin Ronacher puts it: “I would argue that there is probably not a single programmer in in the python Community who could tell you exactly what happens if you add two objects together.”
This lack of certainty is evidenced by projects like PyPy, which attempts to re-implement Python in Python itself. Even though PyPy's core developers aim for bug-for-bug compatibility with CPython and should theoretically get it right, PyPy still occasionally disagrees with CPython’s exact behavior in edge cases. The underlying mechanics are simply too fraught with historical quirks to produce an obvious specification.
- JavaScript explicitly specifies the behavior of binary operators on objects in its language standard. - Python’s binary operations depend on low-level object flags and complex runtime type hierarchies. - PyPy periodically diverges from CPython regarding binary operations because the true behavior is an emergent property of CPython's C implementation rather than a formal specification.
---
[11:54](https://www.youtube.com/watch?v=qCGofLIzX6g&t=714s) Two Parallel Worlds: C Slots and Python Classes
To understand why operator dispatch is so complex, one must look at the concept of a type "slot." In the CPython interpreter, a slot is essentially a function pointer on a C `struct` maintained for every single type. If you inspect an integer from the Python side, you see a class dictionary containing a special method called `__add__` pointing to a callable. However, the C interpreter was designed in the early 1990s, long before these special dunder methods existed. Back around 1995, Python maintained two completely disjoint systems: Python classes and native C types. Built-in types implemented in C had no concept of Python class dictionaries; adding two built-in integers directly executed a C struct function pointer in the number methods suite known as `nb_add`.
Later, support was added for user-defined Python classes, allowing them to participate in operator overloading via callback methods like `__add__`. This produced two completely separate evaluation models. Python eventually attempted to unify them by building a synchronization layer. When a C type defines an `nb_add` slot, CPython uses runtime machinery to expose that slot to Python code as `__add__`. Conversely, when a pure Python class declares an `__add__` method, CPython extracts that function and populates the underlying C type's slot table (`tp_as_number->nb_add`) with a C function pointer that delegates back to the Python method. In theory, `Foo.__add__` and `FooType->tp_as_number->nb_add` are kept in sync, but legacy decisions make clean synchronization impossible.
```mermaid flowchart TD accTitle: C Slots vs Python Special Methods Synchronization accDescr: Diagram illustrating how C-level type slots and Python-level dunder methods attempt to synchronize with one another.
subgraph C_Side["C-Level Type Struct"] CSlot["C Slot: tp_as_number->nb_add"] end
subgraph Python_Side["Python-Level Class Dict"] PyMethod["Special Method: __add__"] end
CSlot -->|"Slot wrapper exposes C func"| PyMethod PyMethod -->|"Internal magic installs wrapper into slot"| CSlot ```
- In early Python (circa 1995), native C types and user-defined Python classes lived in separate worlds with zero shared inheritance. - C types executed arithmetic via specialized C struct slots such as `nb_add`. - User-defined classes handled arithmetic through dunder methods like `__add__`. - Modern CPython continuously tries to keep C struct function pointers (`tp_as_number->nb_add`) and Python dictionary entries (`__add__`) synchronized.
---
[14:03](https://www.youtube.com/watch?v=qCGofLIzX6g&t=843s) Number Addition vs. Sequence Concatenation
The early design of Python introduced another historical rift: the distinction between numbers and sequences. Very early on, Guido van Rossum provided built-in numbers (integers and floats) alongside sequences (strings, lists, and tuples). He opted to create two separate C-level concepts for addition: one for numeric arithmetic and another for sequence concatenation. As a consequence, C structs hold distinct slot suites for numbers (`tp_as_number`) and sequences (`tp_as_sequence`).
Whenever the `+` operator executes, the interpreter must inspect both slots, introducing another vector for desynchronization. While both conceptually map to `__add__` at the Python layer, explaining how they resolve in practice is nearly impossible. Most tutorials take the easy way out and simply claim that `a + b` translates directly to `a.__add__(b)`. More conscientious tutorials might suggest it evaluates as `type(a).__add__(a, b)`. Yet both explanations are fundamentally inaccurate representations of what the virtual machine actually does.
```mermaid flowchart TD accTitle: Real Execution Flow of a + b in CPython accDescr: Flowchart showing the actual series of checks and fallbacks executed by CPython when evaluating a plus b.
Start["Evaluate: a + b"] --> CheckInt{"Are both a and b integers?"} CheckInt -- Yes --> FastInt["Execute specialized fast integer addition"] CheckInt -- No --> CheckStr{"Are both a and b strings?"}
CheckStr -- Yes --> FastStr["Execute specialized string concatenation"] CheckStr -- No --> CheckNumSlot{"Does type(a) implement number slot nb_add?"}
CheckNumSlot -- Yes --> ResolveNum["Resolve number slots based on complex type relationships"] CheckNumSlot -- No --> CheckSeqSlot{"Does type(a) implement sequence slot sq_concat?"}
ResolveNum --> InvokeNum["Invoke nb_add slot"] CheckSeqSlot -- Yes --> InvokeSeq["Invoke sq_concat slot via sequence path"] CheckSeqSlot -- No --> RaiseErr["Raise TypeError"] ```
- Early Python split the `+` operator into numeric addition (`nb_add`) and sequence concatenation (`sq_concat`). - Standard educational material oversimplifies the operation as `a.__add__(b)` or `type(a).__add__(a, b)`. - The real interpreter execution order includes: 1. A fast-path check to see if both operands are pure integers. 2. A fast-path check to see if both operands are strings (for optimized concatenation). 3. Querying whether `a` implements numeric slots, checking `b`'s slots, and applying inheritance precedence rules. 4. Falling back to the sequence concatenation path if numeric addition is not implemented.
---
[16:10](https://www.youtube.com/watch?v=qCGofLIzX6g&t=970s) The Cost of Internal Optimization and Frozen APIs
The contrast between what the interpreter does and what Python syntax suggests is striking. Evaluating `a.__add__(b)` involves looking up the attribute name on the type and calling it directly. In Armin's words: “The scary thing is this thing is easier than this thing.” The internal fast paths and slot resolution systems built to speed up arithmetic are vastly more complicated than a basic method lookup. Because this dictionary-based path is unoptimized and rarely used internally, there are even peculiar situations where manually writing out `a.__add__(b)` runs faster than letting the interpreter evaluate `a + b`.
Over decades of development, CPython accrued layers of optimizations to accelerate operations within this complex architecture. A reasonable engineer might ask why the community does not simply discard the convoluted C dispatch slots, optimize the clean Python method path, and standardize `a + b` as `type(a).__add__(a, b)`. The barrier is that CPython has exposed its low-level implementation details to the external ecosystem for so long that the core team can no longer change it. Armin explains the trap: “And the reason for this is that unfortunately we expose so much of the language to the ecosystem around it that we can't change The Interpreter anymore.”
- Manually executing an explicit dunder method lookup can, in specific scenarios, bypass slot resolution overhead and run faster than using operator syntax. - CPython cannot replace its internal dual-slot mechanism with unified method lookups without breaking the global Python package ecosystem.
---
[17:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1072s) Leaky Internals: Slot Wrappers and Method Descriptors
Anyone who has compiled a C extension is familiar with the low-level nature of Python’s C API, where custom types must explicitly populate raw C struct slot tables. However, these internals leak directly into high-level userland Python code. Even beginners who have never touched C routinely encounter cryptic exceptions referencing internal types, such as errors stating that a `slot_wrapper` has no attribute.
This leakage creates profound semantic discrepancies between native and user-defined types. Defining a class in Python and inspecting its `__add__` method yields an `unbound method` (or a regular function in modern Python). Inspecting the exact same attribute on a built-in C type like `int` yields a `slot_wrapper`. A slot wrapper is an internal CPython construct that wraps a raw C function pointer so Python can invoke it, yet it surfaces as a distinct Python object with unique behaviors.
By comparison, JavaScript avoids this fragmentation: a function is simply a function across the entire language, whether authored in JavaScript or implemented inside the browser engine in C++. Python, however, exposes an enormous menagerie of callable artifacts: - Standard functions (`function`) - Unbound methods / Bound methods (`instancemethod` / `method`) - Slot wrappers (`slot_wrapper`) - Method descriptors (`method_descriptor`) - Built-in functions (`builtin_function_or_method`) - Arbitrary callable objects implementing `__call__`
These types behave differently in subtle, critical ways. For example, if you pull an unbound method off a custom class and assign it as an attribute on another class, it acts as a descriptor, automatically binding `self` when accessed from an instance. If you do the exact same thing with a C-level `slot_wrapper`, it does not bind `self`. Python developers have come to rely on these incidental edge cases in production code. A common idiom is setting a class-level conversion hook using a built-in type:
```python class Record(object): conversion_function = int ```
This pattern only works because `int` is a C type whose call mechanism does not implement method descriptor binding; assigning a normal user-defined function there would cause it to receive `self` as its first parameter and fail. Because developers rely on these subtle differences, CPython can neither clean up its internal type architecture nor eliminate these discrepancies. Every alternative implementation, including PyPy, is forced to painstakingly replicate every single leaky internal detail.
[21:45](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1305s) Leaky C Extensions: The Curious Case of the `re` Regex Object
The internal design of Python is fraught with complexity, and because nearly everyone depends on these behaviors, optimizing the runtime has become virtually impossible. These low-level details do not just affect performance when emulating Python in other languages—they actively shape how developers write Python code. A striking, historical example of this leakage existed in the standard `re` regular expression module prior to Python 2.7. When compiling a regular expression, the runtime returned an internal pattern object (such as `SRE_Pattern`). If a developer attempted to introspect that compiled pattern by accessing `__class__`, Python raised an `AttributeError`. Similarly, accessing `__repr__` directly failed, even though calling `repr()` worked and the object otherwise behaved like a normal Python object.
Armin Ronacher highlights how deeply bizarre this situation was: “The C interpreter sees a different version of this object than Python code would do.” Because the object was written in C, its author bypassed standard Python attribute resolution and customized its behavior purely within the C API. At the C layer, class and representation slots existed, but they were never exposed as Python-level dunder attributes. Consequently, it was impossible to write a pure Python implementation of the built-in `repr()` function that reliably invoked `__repr__` across all standard types, because objects like the compiled regex pattern explicitly disagreed with Python-level introspection. This discrepancy demonstrated how the C API allowed internals to remain completely invisible and inconsistent from the perspective of high-level Python code.
- Compiled regular expression objects in Python versions prior to 2.7 raised `AttributeError` when accessing `__class__` or `__repr__`. - The discrepancy stemmed from C-level object definitions overriding internal struct slots without exposing the corresponding dunder attributes to the Python namespace. - Pure Python emulations of core built-in functions (like `repr()`) could not be fully implemented in Python because certain built-in C objects broke the dunder contract.
---
[23:51](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1431s) Old-Style Class Proxying and the Evolution of `__getattr__`
Another vivid instance of internal implementation leaking into language semantics lies in the difference between old-style and new-style classes, particularly around Python 2.4. In older versions of the language, developers could construct a proxy object using an old-style class that defined `__getattr__` to forward attribute lookups to a wrapped object. If the proxy forwarded calls to an integer like `42`, attempting to evaluate the proxy in the interpreter would successfully call `__repr__` by dispatching through `__getattr__`, finding the wrapped integer's representation function, and returning `"42"`. It operated as a flawless 100% transparent proxy.
Modern Python, however, no longer permits this pattern because special method lookups bypass `__getattr__` and inspect the class dictionary directly. Old-style classes relied on an internal `instance` type that forwarded all special method calls dynamically, matching the original mechanics of early Python interpreters. When Python introduced new-style classes, it preserved old-style behavior through an increasingly complex emulation layer rather than keeping a single, unified dispatch model. Ronacher reflects on this shift: “So when you actually consider how Python as a language was taught to people, at one point this `__getattr__` was actually correct; it just became more complex over the years and now has this massive sort of disagreement in how it works.”
```mermaid flowchart TD accTitle: Evolution of Dunder Method Dispatch in Proxies accDescr: Comparison of old-style proxy dispatch via getattr versus modern class-level slot lookup.
subgraph OldStyle["Old-Style Class Dispatch (Python 2.4)"] A["Proxy Instance"] -->|"Lookup __repr__"| B["__getattr__('__repr__')"] B -->|"Dynamic forward"| C["Wrapped Target (e.g. 42)"] C -->|"Returns target __repr__"| D["Transparent 100% Proxy"] end
subgraph NewStyle["Modern / New-Style Dispatch"] E["Proxy Instance"] -->|"Lookup __repr__"| F["Proxy Type Object Slot"] F -.->|"Bypasses __getattr__"| G["AttributeError / Default Object Repr"] end ```
---
[25:27](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1527s) The History and Flaws of Fixed-Width Unicode Storage
The historical handling of strings in Python provides another clear illustration of how implementation constraints shaped the language. Originally, Python had only byte strings, which were straightforward byte arrays where each character held an integer value from 0 to 255. This structure established a strong expectation among developers: string indexing is $O(1)$, meaning one can immediately access the first, second, or fourth character by offset. When Unicode support was introduced, Python's designers sought to preserve this direct indexing guarantee. Instead of adopting a variable-length byte representation, they modeled Unicode as arrays of fixed-width numbers, initially spanning from 0 to 65,535 (UCS-2), and later allowing builds configured with UCS-4 for larger code points.
This approach was heavily influenced by early standardization efforts. Around 25 years prior, two separate bodies attempted to expand beyond ASCII. The Unicode Consortium initially believed that 65,535 characters would suffice for all the world's writing systems. Simultaneously, an ISO committee pursued a 32-bit standard ($2^{32}$ characters), which stalled because they could not devise an efficient encoding scheme. Eventually, it became apparent that Unicode exceeded 65,535 characters—emojis, in particular, require code points well beyond the 16-bit plane. While UTF-8 emerged as a variable-length solution that efficiently encoded common scripts (ASCII in 1 byte, Cyrillic in 2 bytes), Python 2 stuck with fixed-width build configurations. Under a UCS-2 build, accessing an emoji split the character into two surrogate halves. To process emojis properly, Python had to be compiled with UCS-4, which allocated 4 bytes per character. Ronacher underscores the inefficiency: “If you want to deal with emojis in Python 2 you would have to compile it with UCS-4 which takes then four bytes per character, which is an enormous waste; in particular, it's an enormous waste because the largest character which can ever be in Unicode is only 21 bits I think, yeah 21 bits, which is just slightly above half of it, so you will have the vast majority of your space unused.”
- The Unicode Consortium originally operated on the faulty assumption that 65,535 code points (UCS-2) would cover every character. - Unicode code points max out at 21 bits, meaning UCS-4 builds (32 bits per character) leave massive amounts of allocated memory permanently zeroed out. - Under UCS-2 builds of Python 2, supplementary characters like emojis cannot fit into a single slot and break into surrogate pairs when indexed.
---
[30:03](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1803s) The $O(1)$ Indexing Guarantee vs. Modern Variable-Length UTF-8
The central reason Python endured such massive memory bloat instead of using UTF-8 was its rigid commitment to $O(1)$ random character indexing: “Because we guaranteed that if you access the first character you will get the first character and if you access the second character you'll get the second character, and the only way to do this in UTF-8 would be to scan character by character by character to figure out which one you want to access.” With UCS-2 or UCS-4, the interpreter simply offsets memory by multiplying the index by the fixed width (2 or 4 bytes). Over the preceding decade, however, the industry coalesced around UTF-8 as the de facto standard. Modern systems languages like Go and Rust embraced UTF-8 natively and intentionally omit arbitrary index-based character access, while platforms like Apple's Swift avoid teaching developers that random character indexing is an efficient operation.
Python 3 addressed this problem by implementing a flexible string representation (PEP 393), where each string's backing array adjusts to 1, 2, or 4 bytes depending on the widest character it contains. Even with this optimization, Python stubbornly retains the original guarantee of constant-time character indexing. Ronacher argues that this guarantee was never the outcome of a deliberate architectural plan, observing that “this guarantee is only given to you because the interpreter decided at one point that that is a reasonable thing to do; it was not, I don't think there was a conscious decision in doing this, I think it just happened.” This reflects a recurring theme: the language definition is essentially identical to the CPython interpreter implementation. While this tight coupling hampers optimization and occasionally causes desynchronization, it also grants Python developers a visceral, hands-on intimacy with the underlying machine, contrasting sharply with the sandboxed, high-level abstraction experienced by JavaScript developers.
- In pure UTF-8, retrieving a character at index $N$ requires an $O(N)$ linear scan from the beginning of the string to decode preceding multibyte sequences. - Newer languages such as Go and Rust do not provide constant-time random character access on UTF-8 strings by design. - Python 3 mitigates storage waste via a tiered internal representation (Latin-1, UCS-2, or UCS-4 per string), preserving the historic $O(1)$ indexing contract. - CPython's language boundaries are defined by its concrete implementation, creating a stark contrast with sandbox-oriented runtimes like JavaScript in web browsers.
[32:46](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1966s) The Leaky Power of `sys._getframe()`
Armin Ronacher explains that because Python's interpreter has always shone through so directly, a unique ecosystem developed around its internals. While exposing the interpreter state makes it notoriously hard to eliminate the Global Interpreter Lock (GIL) or implement certain compiler optimizations, it also gives developers immense power. When polling the audience on who uses or has heard of `sys._getframe()`, only a shockingly small number of hands go up. Armin characterizes `sys._getframe()` as the “secret sauce” that makes a lot of Python code spicy—if not necessarily clean. At any point during execution, code can ask the interpreter for the internal frame object of the current call site, exposing nearly the entire interpreter state. From the perspective of language design, this is generally considered completely unacceptable: “If you would go into designing a new high-level language, everybody would argue that never give this thing—this thing is slow, it's ridiculous, just don't expose it, it shouldn't exist.” However, because Python exposed it early on and libraries immediately leaned into it, core developers were forced to keep frame inspection relatively performant, unlike in languages like Ruby where similar mechanisms exist but crawl because nobody relies on them.
A frame object allows runtime inspection and sometimes mutation of local variables, retrieval of the current filename, examination of active exceptions, and walking the call stack. This power has invited both clever utility and sheer abuse across the ecosystem. For example, `zope.interface` used a method called `implements` that walked up one frame from its call site to dynamically inject attributes directly into the enclosing class definition—a technique that makes little sense from a conventional calling perspective. Similarly, the standard library `warnings` module uses stack frames to anchor deprecation warnings two frames above the call site rather than pointing at the warning machinery itself, and the `logging` module skips internal logging frames so log lines report where the user actually called the log function rather than where the logging helper was implemented.
Beyond standard library tricks, this deep frame access underpins critical debugging and observability infrastructure. Armin brings up Twisted Manhole, an obscure tool allowing developers to attach a remote interactive debugger to a long-running, misbehaving Python server process—such as a game world simulation—and inspect state without shutting it down. He notes that this same capability powers Sentry, where he works: when an application crashes, Sentry extracts every local variable, frame duration, and stack layer directly from the live runtime state. Python web frameworks depend heavily on this leakiness; when a Django or Flask application crashes, it displays detailed tracebacks, and Flask even allows developers to execute arbitrary code interactively inside an HTML-rendered traceback frame to inspect the environment.
- `sys._getframe()` returns a frame object exposing runtime local variables, active exceptions, and current execution coordinates. - Core Python developers were forced to optimize frame access because real-world code became dependent on it, unlike in Ruby where equivalent mechanisms remain unoptimized. - `zope.interface` used frame inspection to alter class definitions from within body-level function calls. - The standard `warnings` and `logging` modules inspect callers to skip internal frames and attribute warnings or logs to caller sites. - Twisted Manhole, Sentry, Django debug pages, and Flask's interactive web debugger all fundamentally rely on exposed frame internals.
[37:35](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2255s) `sys.modules` and the Impossibility of Multi-Version Imports
Armin shifts to another deeply exposed piece of interpreter state: `sys.modules`. Whenever Python imports a module, the module object is cached in this dictionary. This ensures that when disparate parts of an application import the same module (such as `cgi`), both call sites receive the exact same instance. While convenient, exposing this dictionary directly caused the broader ecosystem to treat it as an invariant public API. Developers regularly look up classes by dynamically importing a module name and querying `sys.modules`, the standard `copy` module inspects it, and `pickle` relies on it entirely to unpickle and reconstruct serialized objects.
Because the entire ecosystem relies on `sys.modules` being a flat global dictionary mapping simple module names to module instances, modern architectural improvements cannot be introduced without breaking backward compatibility. There have been numerous attempts to allow multiple versions of the same library to coexist within a single Python runtime, but this cannot be implemented without adding version or scope identifiers into `sys.modules`. Serialized data formats lock Python into this legacy behavior forever: “So until pickle dies, we will never have versioned modules—this is disappointing.” Armin points out that Zope built an entire database (ZODB) around pickling objects, meaning any breaking architectural change to `sys.modules` or `pickle` would invalidate decades of persisted database records.
```mermaid flowchart TD accTitle: How sys.modules restricts versioning accDescr: Import requests check sys.modules, caching a single global instance, which prevents loading multiple versions.
subgraph ImportCache["Runtime Import Resolution"] A["import foo"] --> B{"foo in sys.modules?"} B -- Yes --> C["Return cached module instance"] B -- No --> D["Load file & store into sys.modules['foo']"] end
subgraph EcosystemLock["Ecosystem Dependencies"] E["pickle.loads()"] -->|"queries exact module name"| C F["copy.copy() / inspect"] -->|"expects single global instance"| C G["Multiple library versions\n(e.g., foo v1 & foo v2)"] -.->|"BLOCKED: cannot key by version without breaking pickle"| B end ```
[39:53](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2393s) Builtin Types, Static Memory Locations, and Embedding Woes
Armin asks the audience whether they have ever wondered why `int` is reported as a type while an empty subclass of `int` is reported as a class, noting that while Python 3 reconciles the terminology somewhat, the underlying runtime mechanics remain identical. The fundamental distinction is that built-in types live at fixed, static global memory addresses within the CPython binary, whereas user-defined classes sit dynamically allocated on the heap. When CPython performs primitive arithmetic operations—such as adding two numbers together—it needs to check whether both operands are exact integers as quickly as possible. Rather than performing dynamic lookups, the C code simply compares the operand’s type pointer against `&PyInt_Type`, a global variable in C.
Because `PyInt_Type` is a single static global C variable with internal reference counting and mutable state, it cannot be safely shared across isolated interpreter instances running on separate operating system threads within the same process. Armin compares this to how modern web browsers embed JavaScript engines: in a browser, every tab can run completely isolated JavaScript runtimes (like V8) across separate threads without sharing state. In Python, an embedder like Nginx cannot spin up five worker threads running five isolated Python interpreters inside the same process because all instances collide on static C types: “This is, I think, the biggest reason why Python was never a language that was embedded in things, because the interpreter design internally never supported this idea of having multiple interpreters running.” Armin shares that he personally attempted to fix this limitation by refactoring built-in types through a level of indirection so separate threads could host fully isolated interpreters, but the deep-rooted C design made it virtually impossible to pull off.
- Built-in types like `int`, `float`, and `str` are represented by static C structs (such as `PyInt_Type`) located at fixed memory addresses. - CPython performs identity checks (`obj->ob_type == &PyInt_Type`) via raw pointer comparisons for maximum speed during hot operations like integer addition. - Subclasses of primitive types are allocated on the heap, preventing the fast-path static pointer equality check from applying to them. - Because static type structs hold mutable internal state and reference counts, running multiple isolated interpreter runtimes across threads in a single process leads to memory collisions. - This structural limitation prevents Python from being embedded in multi-threaded architectures (like Nginx worker threads or multi-tab web browsers) in the way JavaScript runtimes are embedded.
[43:19](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2599s) The Deep Consequences of Leaky Internals and the Unremovable GIL
Armin Ronacher reflects on the cascading rabbit hole that emerges when developers and the wider ecosystem become dependent on low-level interpreter implementation details. Once these details are published and relied upon, they become impossible to change without breaking backward compatibility. The single most prominent casualty of this architectural legacy is the Global Interpreter Lock (GIL). Removing the GIL is not merely an engineering challenge; it is blocked because foundational interpreter assumptions—such as having a single static integer type shared across everything—prevent running isolated threads with independent interpreter states side by side. Furthermore, Python directly exposes internal reference counting mechanisms to the C API, weaving them into virtually every third-party C extension in existence.
This deep exposure extends upward into the highest layers of the language. For example, implementing cleanly isolated, multi-version libraries remains exceptionally difficult because `sys.modules` is exposed directly as an accessible interpreter detail rather than hidden behind an abstraction. Even the major backward-incompatible migration to Python 3 was unable to undo these foundational patterns. While this design has not completely stopped people from getting productive work done, it imposes a hard ceiling on interpreter speed, parallelism, and concurrency that the community must live with. Reflecting on this systemic trap, Armin notes: “It's something that I understand at this point that Python as a language is just this is how it works and we have to live sort of with this history of how the interpreter works and Python 3 also couldn't really change that, but it's something that maybe as you go in and then discover other languages is something that you wouldn't want to repeat.”
```mermaid flowchart TD accTitle: Cascading Consequences of Leaky Interpreter Internals accDescr: Diagram illustrating how exposing low-level interpreter details locks in the GIL and prevents architectural evolution.
subgraph Exposed["Exposed Internals"] RC["Exposed reference counts in C API"] ST["Shared static types (e.g., integer)"] SM["Publicly mutable sys.modules"] end
subgraph Ecosystem["Ecosystem Lock-in"] EXT["Ubiquitous C extensions rely on refcounts"] MOD["Single global module namespace"] end
subgraph Limitations["Architectural Bottlenecks"] GIL["Inability to remove GIL"] PAR["Impossible multi-interpreter threading"] MV["Difficulty supporting multi-version libraries"] end
RC --> EXT ST --> PAR EXT --> GIL SM --> MOD MOD --> MV ```
[45:20](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2720s) Dynamic Flexibility vs. Speed Ceiling and Closing the Talk
Because the interpreter is bound by these historical constraints, it struggles to parallelize and cannot easily become more dynamic without suffering severe performance penalties. The CPython interpreter is forced to "cheat" with specialized fast paths for operations like integer addition to stay performant despite its internal complexity. In contrast, a language like Ruby retains full dynamic flexibility—theoretically allowing a developer to monkey-patch integer addition into subtraction because it avoids hardcoded fast paths for primitives. In chasing raw execution speed under an overly complex model, Python ended up sacrificing both architectural purity and dynamic capability.
Armin halts his slide progression with self-deprecating humor, admitting that explaining the nuanced mechanics and widespread ramifications of leaky interpreter internals is surprisingly difficult to communicate in a standard lecture format. Acknowledging that the talk drifted into dense technical weeds, he invites the audience to ask questions and take the discussion in any direction they wish, even if it diverges wildly from the prepared presentation slides. As Armin puts it: “I'm rambling through the slides a little bit because I'm realizing that it's actually very hard to convey this overall message that how the interpreter works internally in a sense that is sort of easy to understand and what the ramifications are of it.”
[46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) Q&A: Small Public APIs and Modern Cache-Conscious Architecture
An audience member opens the Q&A by asking whether the fundamental lesson of the talk is that language designers must expose as tiny a public API surface as possible to avoid ecosystem lock-in. Armin enthusiastically agrees, pointing to JavaScript as an exemplary counter-model: engines like V8 keep their internal interpreter abstractions largely inaccessible to runtime code, preventing developers from building workflows dependent on low-level engine quirks. As Armin states: “The correct thing to do for a language is sort of what JavaScript did in many ways, where you keep the public exposed interpreter API very, very small to nonexistent, because then people will never get in this sense where they can start abusing it.”
The attendee follows up with a probing question about performance: if Python abandoned its special-case shortcuts—such as the fast paths in dictionary lookups and arithmetic—wouldn't the language become intolerably slow? Armin argues the opposite. While he understands why historical maintainers added special-casing to boost performance, he contends it was done the wrong way when evaluated against modern hardware architecture. Contemporary CPUs achieve high performance through cache locality, branch prediction, and compact instruction footprints that do not thrash CPU caches. In Python, an overwhelming amount of execution time is spent inside dictionary lookups. Armin argues that if Python drastically simplified the object model and stripped out complex special cases, the interpreter loop would execute significantly less code:
- **Cache utilization and branch prediction**: A minimal codebase fits comfortably in CPU instruction caches, drastically reducing cache misses and branch mispredictions. - **Dynamic flexibility**: Simplifying operations rather than maintaining sprawling manual C optimizations would allow Python to be simultaneously faster and more dynamic. - **The Ruby paradox**: Armin points out the bitter irony of CPython's current design: “I think the irony in this is that we are less dynamic than Ruby, but we are probably slower per opcode than Ruby is as a result of this, because it was obviously slow so people started doing these optimizations for the common cases, but we could have made the uncommon case significantly faster by simplifying it.”
The session closes with appreciative applause from the audience as the host thanks Armin for the insightful deep dive into Python's internals.
## Notable quotes > [10:39](https://www.youtube.com/watch?v=qCGofLIzX6g&t=639s) I would argue that there is probably not a single programmer in in the python Community who could tell you exactly what happens if you add two objects together. — Armin Ronacher
> [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) The correct thing to do for a language is sort of what JavaScript did in many ways, where you keep the public exposed interpreter API very, very small to nonexistent, because then people will never get in this sense where they can start abusing it. — Armin Ronacher
> [16:10](https://www.youtube.com/watch?v=qCGofLIzX6g&t=970s) unfortunately we expose so much of the language to the ecosystem around it that we can't change The Interpreter anymore. — Armin Ronacher
> [37:35](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2255s) until pickle dies, we will never have versioned modules—this is disappointing. — Armin Ronacher
## Mentioned Armin Ronacher, Python, CPython, Flask, Sentry, Django, Lektor, Werkzeug, Jinja2, MarkupSafe, Ruby, JavaScript, PyPy, Guido van Rossum, Go, Rust, Apple, Swift, Zope, ZODB, Twisted Manhole, Nginx, V8
[Source](https://www.youtube.com/watch?v=qCGofLIzX6g)