diff --git a/GLOSSARY.md b/GLOSSARY.md
index 4e5ea6e..71c1310 100644
--- a/GLOSSARY.md
+++ b/GLOSSARY.md
@@ -8,7 +8,7 @@ This file is generated from `pyxray/src/pyxray/glossary.py`. Edit that and run `
## Index
-[ASDL](#asdl) | [Argument Clinic](#argument-clinic) | [EXTENDED_ARG](#extended_arg) | [GC pre header](#gc-pre-header) | [JIT](#jit) | [PEG parser](#peg-parser) | [PyVarObject](#pyvarobject) | [Pyodide](#pyodide) | [WebAssembly](#webassembly) | [abstract syntax tree](#abstract-syntax-tree) | [adaptive instruction](#adaptive-instruction) | [arena](#arena) | [assembler](#assembler) | [backtrace](#backtrace) | [basic block](#basic-block) | [binding](#binding) | [block](#block) | [blurb](#blurb) | [borrowed reference](#borrowed-reference) | [bytecode](#bytecode) | [cell](#cell) | [class cell](#class-cell) | [closure](#closure) | [code generation](#code-generation) | [code object](#code-object) | [coding cookie](#coding-cookie) | [cold block](#cold-block) | [computed goto](#computed-goto) | [configure](#configure) | [constant folding](#constant-folding) | [control flow graph](#control-flow-graph) | [cycle collector](#cycle-collector) | [deallocation](#deallocation) | [debug build](#debug-build) | [deoptimization](#deoptimization) | [devguide](#devguide) | [dispatch](#dispatch) | [environment changed](#environment-changed) | [eval loop](#eval-loop) | [evaluation order](#evaluation-order) | [exception table](#exception-table) | [f string](#f-string) | [finalizer](#finalizer) | [frame](#frame) | [free threaded build](#free-threaded-build) | [free variable](#free-variable) | [gdb](#gdb) | [generated file](#generated-file) | [generation](#generation) | [grammar](#grammar) | [header file](#header-file) | [heap type](#heap-type) | [immortal object](#immortal-object) | [indent and dedent](#indent-and-dedent) | [inline cache](#inline-cache) | [instance dictionary](#instance-dictionary) | [instruction](#instruction) | [interning](#interning) | [left recursion](#left-recursion) | [line table](#line-table) | [magic number](#magic-number) | [marshal](#marshal) | [metaclass](#metaclass) | [monitoring events](#monitoring-events) | [new reference](#new-reference) | [object](#object) | [object header](#object-header) | [obmalloc](#obmalloc) | [oparg](#oparg) | [opcode](#opcode) | [parser generator](#parser-generator) | [pdb](#pdb) | [pointer](#pointer) | [pool](#pool) | [product type](#product-type) | [profile guided optimization](#profile-guided-optimization) | [pseudo instruction](#pseudo-instruction) | [pyc file](#pyc-file) | [pyconfig](#pyconfig) | [reference count](#reference-count) | [reference cycle](#reference-cycle) | [reference leak](#reference-leak) | [regen](#regen) | [regrtest](#regrtest) | [replacement field](#replacement-field) | [resource](#resource) | [scope](#scope) | [segmentation fault](#segmentation-fault) | [short circuiting](#short-circuiting) | [small integer cache](#small-integer-cache) | [soft keyword](#soft-keyword) | [specialization](#specialization) | [stack depth](#stack-depth) | [stack effect](#stack-effect) | [static type](#static-type) | [stolen reference](#stolen-reference) | [struct](#struct) | [sum type](#sum-type) | [symbol table](#symbol-table) | [symbol table pass](#symbol-table-pass) | [t string](#t-string) | [test case](#test-case) | [tier one](#tier-one) | [tier two](#tier-two) | [token](#token) | [tokenizer](#tokenizer) | [trace function](#trace-function) | [type object](#type-object) | [underflow](#underflow) | [value stack](#value-stack) | [weak reference](#weak-reference)
+[ASDL](#asdl) | [Argument Clinic](#argument-clinic) | [EXTENDED_ARG](#extended_arg) | [GC pre header](#gc-pre-header) | [JIT](#jit) | [PEG parser](#peg-parser) | [PyVarObject](#pyvarobject) | [Pyodide](#pyodide) | [WebAssembly](#webassembly) | [abstract syntax tree](#abstract-syntax-tree) | [adaptive instruction](#adaptive-instruction) | [arena](#arena) | [assembler](#assembler) | [backtrace](#backtrace) | [basic block](#basic-block) | [binding](#binding) | [block](#block) | [blurb](#blurb) | [borrowed reference](#borrowed-reference) | [bytecode](#bytecode) | [cell](#cell) | [class cell](#class-cell) | [closure](#closure) | [code generation](#code-generation) | [code object](#code-object) | [coding cookie](#coding-cookie) | [cold block](#cold-block) | [computed goto](#computed-goto) | [configure](#configure) | [constant folding](#constant-folding) | [control flow graph](#control-flow-graph) | [cycle collector](#cycle-collector) | [deallocation](#deallocation) | [debug build](#debug-build) | [deoptimization](#deoptimization) | [devguide](#devguide) | [dispatch](#dispatch) | [environment changed](#environment-changed) | [eval loop](#eval-loop) | [evaluation order](#evaluation-order) | [exception table](#exception-table) | [f string](#f-string) | [finalizer](#finalizer) | [frame](#frame) | [free threaded build](#free-threaded-build) | [free variable](#free-variable) | [gdb](#gdb) | [generated file](#generated-file) | [generation](#generation) | [grammar](#grammar) | [header file](#header-file) | [heap type](#heap-type) | [immortal object](#immortal-object) | [indent and dedent](#indent-and-dedent) | [inline cache](#inline-cache) | [instance dictionary](#instance-dictionary) | [instruction](#instruction) | [interning](#interning) | [left recursion](#left-recursion) | [line table](#line-table) | [magic number](#magic-number) | [marshal](#marshal) | [metaclass](#metaclass) | [monitoring events](#monitoring-events) | [new reference](#new-reference) | [object](#object) | [object header](#object-header) | [obmalloc](#obmalloc) | [oparg](#oparg) | [opcode](#opcode) | [parser generator](#parser-generator) | [pdb](#pdb) | [pointer](#pointer) | [pool](#pool) | [product type](#product-type) | [profile guided optimization](#profile-guided-optimization) | [pseudo instruction](#pseudo-instruction) | [pyc file](#pyc-file) | [pyconfig](#pyconfig) | [reference count](#reference-count) | [reference cycle](#reference-cycle) | [reference leak](#reference-leak) | [regen](#regen) | [regrtest](#regrtest) | [replacement field](#replacement-field) | [resource](#resource) | [scope](#scope) | [segmentation fault](#segmentation-fault) | [short circuiting](#short-circuiting) | [slot](#slot) | [slot wrapper](#slot-wrapper) | [small integer cache](#small-integer-cache) | [soft keyword](#soft-keyword) | [specialization](#specialization) | [stack depth](#stack-depth) | [stack effect](#stack-effect) | [static type](#static-type) | [stolen reference](#stolen-reference) | [struct](#struct) | [sum type](#sum-type) | [symbol table](#symbol-table) | [symbol table pass](#symbol-table-pass) | [t string](#t-string) | [test case](#test-case) | [tier one](#tier-one) | [tier two](#tier-two) | [token](#token) | [tokenizer](#tokenizer) | [trace function](#trace-function) | [type object](#type-object) | [underflow](#underflow) | [value stack](#value-stack) | [weak reference](#weak-reference)
## Reading the source
@@ -642,6 +642,22 @@ Tuples, lists and bytes objects all hold a count of how many items they have, an
Also written variable sized object, `ob_size`. First met in O01. See also [object header](#object-header), [object](#object). In the source: [`Include/object.h:174-178@v3.15.0rc1#PyVarObject`](https://github.com/python/cpython/blob/v3.15.0rc1/Include/object.h#L174-L178).
+### slot
+
+**One of the function pointer fields in a type object.**
+
+`tp_repr`, `tp_hash`, `tp_call` and about seventy others. The interpreter reads them directly, so calling `repr(x)` is a load and an indirect call rather than a dictionary lookup. Python code never assigns to one. It defines a dunder method and a table walk fills the slot in, which is what makes the two spellings feel like the same thing.
+
+Also written type slot. First met in O03. See also [slot wrapper](#slot-wrapper), [type object](#type-object). In the source: [`Objects/typeobject.c:11584-11590@v3.15.0rc1#slotdefs`](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L11584-L11590).
+
+### slot wrapper
+
+**A dunder method that is really a C slot with a Python callable wrapped around it.**
+
+When a type written in C is made ready, `add_operators` walks the slot table and puts one of these into the class dict for every slot that has a function in it. That is where `int.__add__` and `object.__repr__` come from: nobody wrote them as methods, they are `nb_add` and `tp_repr` made callable. `type(int.__add__).__name__` is `wrapper_descriptor`, which is how you tell one from an ordinary method.
+
+Also written `wrapper_descriptor`. First met in O03. See also [slot](#slot), [type object](#type-object). In the source: [`Objects/typeobject.c:12456-12470@v3.15.0rc1#add_operators`](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L12456-L12470).
+
### static type
**A type object written out as a C literal and compiled into the binary.**
diff --git a/README.md b/README.md
index 7e4f6c7..de9417c 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,7 @@ A lesson can also end with a boss fight, which is a problem the text does not so
| F12 | [What ends up on disk](lessons/f12-what-ends-up-on-disk/f12.ipynb) | The other direction: a code object flattened into bytes and read straight back, the sixteen byte header decoded field by field, the type byte and the reference table that make marshal small, a fifty line reader checked against the real one, and a handmade .pyc that imports with no .py anywhere | M3 | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/f12-what-ends-up-on-disk/f12.ipynb) |
| O01 | [The header, byte by byte](lessons/o01-the-header-byte-by-byte/o01.ipynb) | The two fields everybody quotes, read out of your own interpreter with ctypes, and the first of them turning out to be three: a thirty two bit count, sixteen bits named and unused, and the flags that say whether an object can ever be freed, plus why an immortal starts in the middle of the range and where a length lives when a type has one | M4 | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o01-the-header-byte-by-byte/o01.ipynb) |
| O02 | [Following the type pointer](lessons/o02-following-the-type-pointer/o02.ipynb) | The biggest struct in the interpreter, read from Python: what basicsize and itemsize say about an instance and why list breaks the arithmetic, the one flag bit behind cannot set attribute of immutable type, the minus one in tp_dictoffset that is a sentinel rather than an offset, the thirty two bytes reserved in front of every instance of a class you wrote, and the five steps a class statement actually compiles to | M4 | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o02-following-the-type-pointer/o02.ipynb) |
+| O03 | [Dunders and the slots behind them](lessons/o03-dunders-and-slots/o03.ipynb) | Where int.__add__ comes from when nobody wrote it, why setting __repr__ on an instance does nothing and setting it on a class works an hour later, how one __getitem__ makes a class iterable without an __iter__, why a subclass __radd__ beats a base class __add__, and the three steps between defining __eq__ and getting unhashable type | M4 | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o03-dunders-and-slots/o03.ipynb) |
More are landing in order. [lessons/README.md](lessons/README.md) explains how one is put together and how to run them locally.
diff --git a/citations.lock.json b/citations.lock.json
index f429b23..473f3e0 100644
--- a/citations.lock.json
+++ b/citations.lock.json
@@ -695,6 +695,51 @@
"first_line": "void",
"lines": 20
},
+ "Objects/typeobject.c:10524-10530@v3.15.0rc1": {
+ "digest": "bd97cf89d9d7c794",
+ "first_line": "#define SLOT0(FUNCNAME, DUNDER) \\",
+ "lines": 7
+ },
+ "Objects/typeobject.c:10573-10600@v3.15.0rc1": {
+ "digest": "88cff08be482a9fa",
+ "first_line": "#define SLOT1BINFULL(FUNCNAME, TESTFUNC, SLOTNAME, DUNDER, RDUNDER) \\",
+ "lines": 28
+ },
+ "Objects/typeobject.c:11522-11531@v3.15.0rc1": {
+ "digest": "4ad77fa864aa2cae",
+ "first_line": "/*",
+ "lines": 10
+ },
+ "Objects/typeobject.c:11546-11553@v3.15.0rc1": {
+ "digest": "0385bd444c1da88c",
+ "first_line": "#define TPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \\",
+ "lines": 8
+ },
+ "Objects/typeobject.c:11584-11590@v3.15.0rc1": {
+ "digest": "f494591191e7ce56",
+ "first_line": "static pytype_slotdef slotdefs[] = {",
+ "lines": 7
+ },
+ "Objects/typeobject.c:11740-11753@v3.15.0rc1": {
+ "digest": "77bf33636ec9ffc9",
+ "first_line": "MPSLOT(__len__, mp_length, slot_mp_length, wrap_lenfunc,",
+ "lines": 14
+ },
+ "Objects/typeobject.c:12086-12096@v3.15.0rc1": {
+ "digest": "69170f42ee69fe8e",
+ "first_line": "/* Update the slots after assignment to a class (type) attribute. */",
+ "lines": 11
+ },
+ "Objects/typeobject.c:12129-12138@v3.15.0rc1": {
+ "digest": "9e6a8d9ad5b83628",
+ "first_line": "dict. */",
+ "lines": 10
+ },
+ "Objects/typeobject.c:12456-12470@v3.15.0rc1": {
+ "digest": "b3eb73a5e54ec845",
+ "first_line": "add_operators(PyTypeObject *type)",
+ "lines": 15
+ },
"Objects/typeobject.c:2512-2531@v3.15.0rc1": {
"digest": "9ac3593478808a3d",
"first_line": "_PyType_AllocNoTrack(PyTypeObject *type, Py_ssize_t nitems)",
@@ -705,6 +750,16 @@
"first_line": "PyTypeObject PyType_Type = {",
"lines": 6
},
+ "Objects/typeobject.c:8808-8818@v3.15.0rc1": {
+ "digest": "42d8cbff41ef620b",
+ "first_line": "static int",
+ "lines": 11
+ },
+ "Objects/typeobject.c:9370-9391@v3.15.0rc1": {
+ "digest": "ff402e2b47b1287a",
+ "first_line": "tp_dict['__hash__'] equal to None.",
+ "lines": 22
+ },
"Objects/weakrefobject.c:1001-1024@v3.15.0rc1": {
"digest": "2990efebc60f458b",
"first_line": "/* This function is called by the tp_dealloc handler to clear weak references.",
diff --git a/lessons/CLAIMS.md b/lessons/CLAIMS.md
index 02289d6..ae6c6a0 100644
--- a/lessons/CLAIMS.md
+++ b/lessons/CLAIMS.md
@@ -12,7 +12,7 @@ header, what the allocator does with a freed block, the shape of the eval loop.
marked with the reason, and a lesson is allowed at most 3 of them. The cap is the point.
Without it the exception becomes the rule and this goes back to being a book.
-257 claims across 30 lessons, 21 of them not observable from Python.
+263 claims across 31 lessons, 21 of them not observable from Python.
## B01. Building CPython, and whether you need to
@@ -207,6 +207,17 @@ Without it the exception becomes the rule and this goes back to being a book.
| an instance of a class with a managed dict is allocated with room for its attribute values after the object, and that room is in neither of the numbers getsizeof adds together | not observable from Python: the inline values array is sized from a table on the type and no Python level call reports it |
| a class statement compiles to a call to __build_class__, and calling type with a name, bases and a namespace produces a type with the same flags, size and mro | [`o02-20`](o02-following-the-type-pointer/o02.ipynb) |
+## O03. Dunders and the slots behind them
+
+| Claim | Proved by |
+| --- | --- |
+| the dunder methods on a builtin type are slot wrappers generated from its C slots, and they are a different kind of object from both the plain methods on the same type and the functions you write in a class body | [`o03-07`](o03-dunders-and-slots/o03.ipynb) |
+| a dunder assigned to an instance is a real attribute you can call by name, and the built in function that would use it never sees it, because the slot looks the name up on the type | [`o03-11`](o03-dunders-and-slots/o03.ipynb) |
+| assigning a dunder to a class updates the slot at once, on that class and on every subclass of it, and deleting it puts the inherited behaviour back | [`o03-13`](o03-dunders-and-slots/o03.ipynb) |
+| a class defining only __getitem__ can be iterated over and used with in, because that one name fills the sq_item slot and the old sequence protocol is still what iteration falls back on | [`o03-15`](o03-dunders-and-slots/o03.ipynb) |
+| a subclass that defines __radd__ itself is called before the base class __add__, and an unrelated class defining __radd__ is not, because both names share one slot and the dispatcher checks the subclass relationship first | [`o03-17`](o03-dunders-and-slots/o03.ipynb) |
+| a class that defines __eq__ and not __hash__ ends up with a real None stored under __hash__ in its class dict, and putting a hash back is a one line assignment | [`o03-19`](o03-dunders-and-slots/o03.ipynb) |
+
## T01. One line, seven stages
| Claim | Proved by |
diff --git a/lessons/README.md b/lessons/README.md
index 0a58f05..935a127 100644
--- a/lessons/README.md
+++ b/lessons/README.md
@@ -34,6 +34,7 @@ Each lesson is a notebook you can run. There is nothing to install and nothing t
| [F12. What ends up on disk](f12-what-ends-up-on-disk/f12.ipynb) | What a .pyc actually holds, the four header fields and which two decide staleness, why half the magic number is a carriage return and a newline, the one byte that names every marshalled object, how a repeated value costs five bytes, a marshal reader written here, and a .pyc assembled by hand and imported with no source file on disk | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/f12-what-ends-up-on-disk/f12.ipynb) |
| [O01. The header, byte by byte](o01-the-header-byte-by-byte/o01.ipynb) | Reading the object header out of memory with ctypes and checking every field against something Python will tell you another way, the thirty two bit count and the two fields sharing its word, why an immortal object parks at 3 << 30 rather than at the top, the two flag bits that say how it got there, why a string CPython uses itself is immortal and yours is not, and the two words in front of the header that only the collector sees | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o01-the-header-byte-by-byte/o01.ipynb) |
| [O02. Following the type pointer](o02-following-the-type-pointer/o02.ipynb) | Where the second word of an object goes, why type is its own type, the two numbers a type keeps about how big its instances are and the one type they do not predict, the flag bit that decides whether you can assign to a type, the negative offsets that are not offsets and the space reserved in front of every instance, and the class statement turning out to be an ordinary function call | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o02-following-the-type-pointer/o02.ipynb) |
+| [O03. Dunders and the slots behind them](o03-dunders-and-slots/o03.ipynb) | The table of ninety four rows that connects every dunder name to a C slot, read forwards when you write a class and backwards when a type written in C is made ready, why a dunder set on an instance is ignored and one assigned to a class takes effect at once, the names that fill two slots and give you truthiness and iteration for free, the two names that share one slot and have to argue about who goes first, and the two mechanisms that put a literal None in __hash__ | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o03-dunders-and-slots/o03.ipynb) |
## The three programs
diff --git a/lessons/o03-dunders-and-slots/build.py b/lessons/o03-dunders-and-slots/build.py
new file mode 100644
index 0000000..50528fd
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/build.py
@@ -0,0 +1,377 @@
+#!/usr/bin/env python
+"""O03. Dunders and the slots behind them.
+
+The third lesson of the object model part. O02 opened the type struct and skipped past the
+seventy odd function pointer fields in the middle of it. This lesson is about those fields and
+about the one table that connects them to the names you write.
+
+Python code never assigns to `tp_repr`. It defines `__repr__` and something fills the slot in.
+That something is a table of ninety four rows in typeobject.c, and it is walked in both
+directions: forwards when you define a class, backwards when a type written in C is made ready
+and its slots have to be exposed as attributes.
+
+Almost every rule people learn about dunder methods as a separate quirk falls out of that one
+table. A dunder set on an instance does nothing. `__len__` gives you truthiness. `__getitem__`
+gives you iteration. Defining `__eq__` makes your class unhashable. All four are the same
+mechanism seen from different angles, and all four are checkable in a few lines.
+
+Run this file to regenerate the notebook, or `just build-lessons` to regenerate all of them.
+`just lessons` checks that the committed notebook still matches this file.
+"""
+
+from nbbuild import BANNER, Lesson
+from nbdiagram import Diagrams
+
+lesson = Lesson("o03-dunders-and-slots", "o03")
+badge = lesson.badge
+cite = lesson.cite
+term = lesson.term
+figure = Diagrams("o03-dunders-and-slots").figure
+
+
+lesson.md(f"""
+# O03. Dunders and the slots behind them
+
+{badge}
+
+O02 opened the type struct and walked past the interesting part. In the middle of it are about seventy fields that are function pointers: `tp_repr`, `tp_hash`, `tp_call`, `tp_iter` and the rest. The interpreter reads those directly. When you write `repr(x)`, C code reaches into `Py_TYPE(x)->tp_repr` and calls whatever is there.
+
+But you never set `tp_repr`. You write `__repr__` and it works. Something in between knows those two names belong together, and that something is a table.
+
+{figure("the-table-runs-both-ways", "the same table read forwards for a Python class and backwards for a C type")}
+""")
+
+
+lesson.md("""
+## About the source references
+
+Now and then this lesson points at CPython's own source, like this: `Objects/typeobject.c:11584-11590@v3.15.0rc1#slotdefs`.
+
+Read it as three parts: the file, the lines, and the release those line numbers belong to. Sometimes there is a fourth part after a `#`, which is the name of the thing those lines are inside.
+
+Every reference is a link, and every one is checked against the pinned source on each change, so a stale reference fails the build instead of sending you somewhere wrong. You never have to read any of it. The references are there so you can go deeper when you want to, and so you can check that this lesson is not making things up.
+
+## Setup
+
+Colab does not come with the small package these lessons use, so the next cell installs it. If you are running this from a checkout of the repository it is already installed and the cell does nothing.
+""")
+
+
+lesson.code("""
+import sys
+
+if sys.version_info < (3, 14):
+ print("This lesson needs CPython 3.14 or newer.")
+ print(f"This runtime is {sys.version.split()[0]}, and the cells below will not run on it.")
+else:
+ try:
+ import pyxray
+ except ImportError:
+ %pip install -q "pyxray @ git+https://github.com/tamnd/cpython-internals@main#subdirectory=pyxray"
+ import pyxray
+""")
+
+
+lesson.md("""
+## Which Python is this
+
+Everything below was checked against the version this cell prints and against 3.14. The two agree on every line of output in this lesson.
+""")
+
+
+lesson.code(
+ """
+import pyxray
+
+pyxray.show()
+""",
+ differs=BANNER,
+ quiet=True,
+)
+
+
+lesson.md(f"""
+## Ninety four rows
+
+The table is called `slotdefs`, {cite("Objects/typeobject.c:11584-11590@v3.15.0rc1#slotdefs")}, and each row is written with one of a dozen small macros, {cite("Objects/typeobject.c:11546-11553@v3.15.0rc1#TPSLOT")}. A row holds the dunder name, the byte offset of the slot inside `PyHeapTypeObject`, a function to install when Python defines the name, a wrapper to expose when C defines the slot, and a docstring.
+
+{figure("one-row-of-the-table", "the five things one row of slotdefs holds")}
+
+The table is sorted by that offset rather than by name, which is the first clue that it is meant to be walked rather than searched.
+
+Reading it backwards is a function called `add_operators`, {cite("Objects/typeobject.c:12456-12470@v3.15.0rc1#add_operators")}. It runs when a type is made ready, walks every row, and for each slot that has a function in it puts a {term("slot wrapper")} in the type's dict under the dunder name. That is where `int.__add__` comes from. Nobody wrote it. It is `nb_add` wrapped so Python can call it.
+
+You can see the result on `object`. Sort its attributes by what kind of thing they are and the slot wrappers stand out from the ordinary methods, which came from a different table entirely.
+
+{lesson.claim("the dunder methods on a builtin type are slot wrappers generated from its C slots, and they are a different kind of object from both the plain methods on the same type and the functions you write in a class body")}
+""")
+
+
+lesson.code(
+ """
+kinds = {}
+for name in dir(object):
+ kinds.setdefault(type(getattr(object, name)).__name__, []).append(name)
+
+for kind, found in sorted(kinds.items()):
+ print(f" {kind:26} {len(found):>2} {' '.join(sorted(found)[:3])}")
+"""
+)
+
+
+lesson.md("""
+Three of those kinds matter here. A `wrapper_descriptor` came out of a slot. A `method_descriptor` came out of the type's ordinary method table, which is a different table with no slots in it. And a plain `function` is what you get when you write the method yourself in a class body.
+
+Line them up next to each other and the difference is one word of output.
+""")
+
+
+lesson.code(
+ """
+class P:
+ def __repr__(self):
+ return "p"
+
+
+for label, value in [
+ ("object.__repr__", object.__repr__),
+ ("int.__add__", int.__add__),
+ ("P.__repr__", P.__repr__),
+]:
+ print(f" {label:18} {type(value).__name__}")
+
+print()
+print(f" object.__repr__.__objclass__ {object.__repr__.__objclass__}")
+print(" so the wrapper remembers which type's slot it came out of")
+"""
+)
+
+
+lesson.md(f"""
+## Why a dunder on an instance does nothing
+
+Now the other direction. When you write a class in Python, your `__repr__` goes into the class dict as an ordinary function. Then `fixup_slot_dispatchers` walks the same table, {cite("Objects/typeobject.c:12129-12138@v3.15.0rc1#fixup_slot_dispatchers")}, and for every row whose name is in your class dict it puts the row's dispatcher into the slot.
+
+For `__repr__` that dispatcher is `slot_tp_repr`, and it is three lines long, {cite("Objects/typeobject.c:10524-10530@v3.15.0rc1#SLOT0")}. All it does is look the dunder name up and call it.
+
+The important word is where it looks. It looks on the type.
+
+{figure("looked-up-on-the-type", "repr going through the slot to a lookup on the type, with the instance dict never consulted")}
+
+That is the whole explanation for a thing that catches everybody at least once. Setting `__repr__` on an instance puts it in the instance dict, where `repr()` will never look, because `repr()` goes through the slot and the slot goes to the type. The attribute is really there. You can call it by hand. The built in function still ignores it.
+
+{lesson.claim("a dunder assigned to an instance is a real attribute you can call by name, and the built in function that would use it never sees it, because the slot looks the name up on the type")}
+""")
+
+
+lesson.code(
+ """
+class Q:
+ pass
+
+
+q = Q()
+q.__repr__ = lambda: "set on the instance"
+
+print(f" it really is in the instance dict {'__repr__' in q.__dict__}")
+print(f" calling it by name works {q.__repr__()}")
+print(f" and repr(q) ignores it completely {repr(q).startswith('<')}")
+"""
+)
+
+
+lesson.md(f"""
+The same machinery runs the other way when you assign to the class instead. `type.__setattr__` calls `update_slot`, {cite("Objects/typeobject.c:12086-12096@v3.15.0rc1#update_slot")}, which finds every row of the table with that name and refills the matching slot, on the type and on all of its subclasses.
+
+So this is not something that happens once at class creation time. Assign `__repr__` to a class an hour later and `repr()` changes immediately, for that class and for everything that inherits from it. Delete it and the slot goes back to what it inherited.
+
+{lesson.claim("assigning a dunder to a class updates the slot at once, on that class and on every subclass of it, and deleting it puts the inherited behaviour back")}
+""")
+
+
+lesson.code(
+ """
+class R:
+ pass
+
+
+class Child(R):
+ pass
+
+
+print(f" to start with repr is the default: {repr(R()).startswith('<')}")
+
+R.__repr__ = lambda self: "the one I just assigned"
+mine, inherited = repr(R()), repr(Child())
+print(f" after assigning {mine}")
+print(f" and the subclass {inherited}")
+
+del R.__repr__
+print(f" after deleting repr is the default again: {repr(R()).startswith('<')}")
+"""
+)
+
+
+lesson.md(f"""
+## One name, several slots
+
+The table is not a one to one mapping and the comment above it says so, {cite("Objects/typeobject.c:11522-11531@v3.15.0rc1")}. Several names can point at one slot, and one name can appear in several rows.
+
+{figure("one-name-many-slots", "four rows of the mapping and what each one buys you")}
+
+`__len__` is the easy case. It appears twice, once as `mp_length` and once as `sq_length`, {cite("Objects/typeobject.c:11740-11753@v3.15.0rc1#MPSLOT")}, so defining it once fills both. That is why a class with only a `__len__` gets truthiness for free: `bool()` has no `__bool__` to call, so it falls back on the length.
+
+`__getitem__` is the fun one. It fills `mp_subscript` and `sq_item`, and `sq_item` is the old sequence protocol, the one that predates iterators. Anything that wants to iterate will use it if there is no `__iter__`, asking for item 0, item 1, item 2 and so on until it gets an `IndexError`. So a class with a single `__getitem__` and nothing else is iterable, and supports `in`, without either of the dunders you would expect to need.
+
+{lesson.claim("a class defining only __getitem__ can be iterated over and used with in, because that one name fills the sq_item slot and the old sequence protocol is still what iteration falls back on")}
+""")
+
+
+lesson.code(
+ """
+class Countdown:
+ def __getitem__(self, index):
+ if index > 4:
+ raise IndexError(index)
+ return 10 - index
+
+
+it = Countdown()
+
+print(f" Countdown has no __iter__ {'__iter__' not in dir(Countdown)}")
+print(f" and no __contains__ {'__contains__' not in dir(Countdown)}")
+print(f" and yet list(it) is {list(it)}")
+print(f" and 7 in it is {7 in it}")
+
+
+class Sized:
+ def __len__(self):
+ return 3
+
+
+print()
+print(f" Sized has no __bool__ {'__bool__' not in dir(Sized)}")
+print(f" and bool(Sized()) is {bool(Sized())}")
+"""
+)
+
+
+lesson.md(f"""
+## One slot, two names
+
+The other direction is more interesting, because when two names share a slot the slot has to decide between them at call time.
+
+`__add__` and `__radd__` both fill `nb_add`. There is only one pointer, so `a + b` cannot dispatch on which method exists. Instead the dispatcher is generated by a macro that has the whole decision written into it, {cite("Objects/typeobject.c:10573-10600@v3.15.0rc1#SLOT1BINFULL")}, and it asks two questions before it will consider the reflected call.
+
+{figure("who-goes-first", "the two questions the shared slot asks before running the reflected method")}
+
+First, is the right hand type a subclass of the left hand type. Second, does the right hand type actually define `__radd__` itself rather than inheriting it. Only if both are true does `__radd__` go first. Otherwise `__add__` runs, and `__radd__` gets its ordinary turn afterwards if `__add__` returns `NotImplemented`.
+
+The second question is the one worth remembering. The macro calls `method_is_overloaded`, and inheriting `__radd__` from a shared base does not count as overloading it.
+
+{lesson.claim("a subclass that defines __radd__ itself is called before the base class __add__, and an unrelated class defining __radd__ is not, because both names share one slot and the dispatcher checks the subclass relationship first")}
+""")
+
+
+lesson.code(
+ """
+class Money:
+ def __add__(self, other):
+ return "Money.__add__ ran"
+
+
+class Extra(Money):
+ def __radd__(self, other):
+ return "Extra.__radd__ ran"
+
+
+class Other:
+ def __radd__(self, other):
+ return "Other.__radd__ ran"
+
+
+print(f" Money() + Other() {Money() + Other()}")
+print(f" Money() + Money() {Money() + Money()}")
+print(f" Money() + Extra() {Money() + Extra()}")
+print()
+print(" the third line is the odd one, and it is odd on purpose:")
+print(" a subclass gets to answer for itself before its base does")
+"""
+)
+
+
+lesson.md(f"""
+## Why defining __eq__ takes __hash__ away
+
+This one usually gets taught as a rule to memorise. It is not a rule, it is two pieces of slot machinery meeting.
+
+{figure("how-eq-loses-hash", "the three steps between defining eq and getting None in hash")}
+
+When a new type inherits its slots from its base, the comparison slots get special treatment, and the question asked is `overrides_hash`, {cite("Objects/typeobject.c:8808-8818@v3.15.0rc1#overrides_hash")}. It looks for `__eq__` in the class dict, and if it does not find that it looks for `__hash__`. If either is there, neither `tp_richcompare` nor `tp_hash` is copied down from the base.
+
+Then, later in making the type ready, `type_ready_set_hash` finds `tp_hash` still empty and no `__hash__` in the dict, {cite("Objects/typeobject.c:9370-9391@v3.15.0rc1#type_ready_set_hash")}. So it sets the slot to `PyObject_HashNotImplemented` and writes a real `None` into the class dict.
+
+That last part is why the effect is so visible. Your class does not merely fail to inherit a hash. It has an actual `__hash__` attribute whose value is `None`, put there by C code, and you can look at it.
+
+{lesson.claim("a class that defines __eq__ and not __hash__ ends up with a real None stored under __hash__ in its class dict, and putting a hash back is a one line assignment")}
+""")
+
+
+lesson.code(
+ """
+class Same:
+ def __eq__(self, other):
+ return True
+
+
+class SameHashed:
+ def __eq__(self, other):
+ return True
+
+ __hash__ = object.__hash__
+
+
+print(f" Same.__hash__ is {Same.__hash__}")
+print(f" and it is really in the dict: {'__hash__' in Same.__dict__}")
+
+try:
+ hash(Same())
+except TypeError as error:
+ print(f" hash(Same()) raises TypeError: {error}")
+
+print()
+print(f" SameHashed put one back, and it works: {isinstance(hash(SameHashed()), int)}")
+"""
+)
+
+
+lesson.md("""
+## Try it yourself
+
+Three things to poke at.
+
+Find every name that gives you a slot wrapper on some builtin type but a plain function on a class you write. Walk `dir(list)` and `dir(str)`, check the type of each attribute, and you have most of the table's left hand column without ever opening the C file.
+
+Define a class with `__getitem__` that never raises `IndexError`, then call `list()` on it and be ready to interrupt. The old sequence protocol has no idea how long anything is, and the only thing that stops it is the exception. This is why `__iter__` was worth adding.
+
+Take the `Money` and `Extra` example and make `Extra` inherit `__radd__` from a shared base instead of defining it. The answer changes, because the dispatcher asks whether the subclass defines the method itself.
+
+## What just happened
+
+There are about seventy function pointer slots in a type object and a table of ninety four rows connecting them to dunder names. Everything in this lesson is that table being read in one direction or the other.
+
+Backwards, when a C type is made ready: `add_operators` turns each filled slot into a slot wrapper in the class dict, which is where `int.__add__` and `object.__repr__` come from.
+
+Forwards, when you write a class: `fixup_slot_dispatchers` puts a generic dispatcher into each slot whose name you defined. That dispatcher looks the name up on the type every time it runs, which is why a dunder set on an instance is ignored and why assigning one to a class takes effect at once, on that class and on its subclasses.
+
+The mapping is not one to one either way. `__len__` fills two slots, so it gives you truthiness. `__getitem__` fills two slots, so it gives you iteration and `in`. And `__add__` and `__radd__` share one slot, so the dispatcher has the subclass rule baked into it.
+
+The `__eq__` and `__hash__` link is two mechanisms meeting: `overrides_hash` stops the slot being inherited, and `type_ready_set_hash` writes a literal `None` in its place.
+
+## What is next
+
+O04 is the method resolution order. Everything in this lesson assumed there was one obvious base class to inherit a slot from. With more than one base there is not, and CPython uses an algorithm with a name and a paper behind it to put the bases in an order that makes inheritance mean something. It also refuses to build the class at all when no such order exists, and seeing that failure is the fastest way to understand what the algorithm is protecting.
+""")
+
+
+raise SystemExit(lesson.save())
diff --git a/lessons/o03-dunders-and-slots/diagrams.py b/lessons/o03-dunders-and-slots/diagrams.py
new file mode 100644
index 0000000..06b7607
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+"""The diagrams for O03, the table that connects dunder names to C slots.
+
+Each scene is written out twice, as an editable `.excalidraw` and as the `.svg` the lesson
+embeds. Run this file to regenerate them, or `just build-diagrams` for every lesson.
+
+The one carrying the lesson is `the-table-runs-both-ways`. There is a single table of ninety
+four rows in typeobject.c, and every rule people learn as a separate quirk of dunder methods
+comes out of one of its rows being read in one direction or the other.
+"""
+
+from nbdiagram import Gallery, figures
+
+gallery = Gallery("o03-dunders-and-slots")
+
+gallery.add(
+ figures.compare(
+ "the-table-runs-both-ways",
+ (
+ "you wrote the class in Python",
+ [
+ "your __repr__ goes in the class dict",
+ "fixup_slot_dispatchers reads it",
+ "tp_repr gets a generic dispatcher",
+ "which looks your function back up",
+ ],
+ ),
+ (
+ "the type was written in C",
+ [
+ "tp_repr already has a function",
+ "add_operators reads it",
+ "__repr__ appears in the class dict",
+ "as a wrapper around the C one",
+ ],
+ ),
+ title="One table, read forwards when you write a class and backwards when C does",
+ verdict="Neither direction is a special case. Both walk the same ninety four rows.",
+ verdict_tone="focus",
+ )
+)
+
+
+gallery.add(
+ figures.stack(
+ "one-row-of-the-table",
+ [
+ "the name, as a string and as an interned object",
+ "the offset of the slot inside PyHeapTypeObject",
+ "the dispatcher to install when Python defines it",
+ "the wrapper to expose when C defines it",
+ "the docstring that wrapper will carry",
+ ],
+ title="What one row of slotdefs holds",
+ note="The offset is why the table is sorted by slot rather than alphabetically by name.",
+ )
+)
+
+
+gallery.add(
+ figures.flow(
+ "looked-up-on-the-type",
+ [
+ "repr(x) in your code",
+ "the C function reads Py_TYPE(x)->tp_repr",
+ "which is slot_tp_repr for a Python class",
+ "that looks __repr__ up on the type",
+ "and calls it with x",
+ ],
+ title="Why a dunder set on an instance is never called",
+ labels=[
+ "the instance dict is not consulted at all",
+ "one dispatcher for every Python class",
+ "on the type, and only on the type",
+ ],
+ tones=["input", "focus", "intermediate", "focus", "durable"],
+ )
+)
+
+
+gallery.add(
+ figures.table(
+ "one-name-many-slots",
+ ["what you write", "what it fills", "and what that buys you"],
+ [
+ ["__eq__ and five others", "tp_richcompare", "one slot, an op argument picks"],
+ ["__len__", "mp_length and sq_length", "len and truthiness both work"],
+ ["__getitem__", "mp_subscript and sq_item", "and the class becomes iterable"],
+ ["__add__ and __radd__", "nb_add", "one slot decides who goes first"],
+ ],
+ title="The mapping is not one to one in either direction",
+ caption="Every surprise about dunder methods is one of these four rows.",
+ tones=["quiet", "focus", "focus", "warning"],
+ )
+)
+
+
+gallery.add(
+ figures.flow(
+ "who-goes-first",
+ [
+ "self + other, and nb_add dispatches both",
+ "is other's type a subclass of self's type",
+ "and does other define __radd__ itself",
+ "if both, __radd__ first, otherwise __add__",
+ ],
+ title="How one slot decides between __add__ and __radd__",
+ labels=[
+ "so the reflected call is worth considering",
+ "inheriting it from a shared base does not count",
+ ],
+ tones=["input", "intermediate", "intermediate", "durable"],
+ )
+)
+
+
+gallery.add(
+ figures.flow(
+ "how-eq-loses-hash",
+ [
+ "you define __eq__ and not __hash__",
+ "overrides_hash sees __eq__ in the class dict",
+ "so tp_hash is not copied down from the base",
+ "type_ready_set_hash finds it still empty",
+ "and writes __hash__ = None into the class dict",
+ ],
+ title="Three steps between defining __eq__ and unhashable type",
+ labels=[
+ "which is the only question it asks",
+ "and neither is tp_richcompare",
+ "so the None you see is real, not a rule",
+ ],
+ tones=["input", "focus", "intermediate", "intermediate", "warning"],
+ )
+)
+
+
+raise SystemExit(gallery.save())
diff --git a/lessons/o03-dunders-and-slots/diagrams/how-eq-loses-hash.excalidraw b/lessons/o03-dunders-and-slots/diagrams/how-eq-loses-hash.excalidraw
new file mode 100644
index 0000000..123afb9
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/how-eq-loses-hash.excalidraw
@@ -0,0 +1,746 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "79b9214277d8185071c6",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 757.4399999999999,
+ "height": 30.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "Three steps between defining __eq__ and unhashable type",
+ "originalText": "Three steps between defining __eq__ and unhashable type",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "e373b7094c9ec36c2a20",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 50.0,
+ "width": 550.6999999999999,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#1971c2",
+ "backgroundColor": "#a5d8ff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "eb86198bc55596eb364d",
+ "type": "text"
+ },
+ {
+ "id": "0b355871ce21d98c6981",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "eb86198bc55596eb364d",
+ "type": "text",
+ "x": 16.0,
+ "y": 72.5,
+ "width": 518.6999999999999,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "you define __eq__ and not __hash__",
+ "originalText": "you define __eq__ and not __hash__",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "e373b7094c9ec36c2a20",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "d4f88f6ded6a5c1fd839",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 180.0,
+ "width": 550.6999999999999,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#e8590c",
+ "backgroundColor": "#ffd8a8",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "7739129f9a8d13cd06b2",
+ "type": "text"
+ },
+ {
+ "id": "0b355871ce21d98c6981",
+ "type": "arrow"
+ },
+ {
+ "id": "37cf5a289296e58f50eb",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "7739129f9a8d13cd06b2",
+ "type": "text",
+ "x": 16.0,
+ "y": 202.5,
+ "width": 518.6999999999999,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "overrides_hash sees __eq__ in the class dict",
+ "originalText": "overrides_hash sees __eq__ in the class dict",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "d4f88f6ded6a5c1fd839",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "b33b0f98bf089b264eaa",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 310.0,
+ "width": 550.6999999999999,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "a67fe762e05b82fa30e0",
+ "type": "text"
+ },
+ {
+ "id": "37cf5a289296e58f50eb",
+ "type": "arrow"
+ },
+ {
+ "id": "d35d5197419e25a8e26a",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "a67fe762e05b82fa30e0",
+ "type": "text",
+ "x": 16.0,
+ "y": 332.5,
+ "width": 518.6999999999999,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "so tp_hash is not copied down from the base",
+ "originalText": "so tp_hash is not copied down from the base",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "b33b0f98bf089b264eaa",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "da843edfb826f0a020fb",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 440.0,
+ "width": 550.6999999999999,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "5275ac82e39a74891d1f",
+ "type": "text"
+ },
+ {
+ "id": "d35d5197419e25a8e26a",
+ "type": "arrow"
+ },
+ {
+ "id": "11e04b7d27c923743406",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "5275ac82e39a74891d1f",
+ "type": "text",
+ "x": 16.0,
+ "y": 462.5,
+ "width": 518.6999999999999,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "type_ready_set_hash finds it still empty",
+ "originalText": "type_ready_set_hash finds it still empty",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "da843edfb826f0a020fb",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "040588c0630ddb3a06e7",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 570.0,
+ "width": 550.6999999999999,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#e03131",
+ "backgroundColor": "#ffc9c9",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "bfab4d4a3a3e7c02bfc4",
+ "type": "text"
+ },
+ {
+ "id": "11e04b7d27c923743406",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "bfab4d4a3a3e7c02bfc4",
+ "type": "text",
+ "x": 16.0,
+ "y": 592.5,
+ "width": 518.6999999999999,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "and writes __hash__ = None into the class dict",
+ "originalText": "and writes __hash__ = None into the class dict",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "040588c0630ddb3a06e7",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "0b355871ce21d98c6981",
+ "type": "arrow",
+ "x": 275.34999999999997,
+ "y": 120.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "e373b7094c9ec36c2a20",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "d4f88f6ded6a5c1fd839",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "fc7c69a4c667ac6eba1a",
+ "type": "text",
+ "x": 287.34999999999997,
+ "y": 140.0,
+ "width": 303.36,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "which is the only question it asks",
+ "originalText": "which is the only question it asks",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "37cf5a289296e58f50eb",
+ "type": "arrow",
+ "x": 275.34999999999997,
+ "y": 250.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "d4f88f6ded6a5c1fd839",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "b33b0f98bf089b264eaa",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "5dc21f2c5b274360eb9b",
+ "type": "text",
+ "x": 287.34999999999997,
+ "y": 270.0,
+ "width": 255.36,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "and neither is tp_richcompare",
+ "originalText": "and neither is tp_richcompare",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "d35d5197419e25a8e26a",
+ "type": "arrow",
+ "x": 275.34999999999997,
+ "y": 380.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "b33b0f98bf089b264eaa",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "da843edfb826f0a020fb",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "0bc217107004f675bd4e",
+ "type": "text",
+ "x": 287.34999999999997,
+ "y": 400.0,
+ "width": 345.59999999999997,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "so the None you see is real, not a rule",
+ "originalText": "so the None you see is real, not a rule",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "11e04b7d27c923743406",
+ "type": "arrow",
+ "x": 275.34999999999997,
+ "y": 510.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "da843edfb826f0a020fb",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "040588c0630ddb3a06e7",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
diff --git a/lessons/o03-dunders-and-slots/diagrams/how-eq-loses-hash.svg b/lessons/o03-dunders-and-slots/diagrams/how-eq-loses-hash.svg
new file mode 100644
index 0000000..77c1635
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/how-eq-loses-hash.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o03-dunders-and-slots/diagrams/looked-up-on-the-type.excalidraw b/lessons/o03-dunders-and-slots/diagrams/looked-up-on-the-type.excalidraw
new file mode 100644
index 0000000..665762a
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/looked-up-on-the-type.excalidraw
@@ -0,0 +1,746 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "4cdadebeb06a5375ef2d",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 642.24,
+ "height": 30.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "Why a dunder set on an instance is never called",
+ "originalText": "Why a dunder set on an instance is never called",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "ef613f0c38fa2b12c09f",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 50.0,
+ "width": 469.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#1971c2",
+ "backgroundColor": "#a5d8ff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "aa7aacffb6e0d68d5ee8",
+ "type": "text"
+ },
+ {
+ "id": "8c30f71d3615b60941ee",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "aa7aacffb6e0d68d5ee8",
+ "type": "text",
+ "x": 16.0,
+ "y": 72.5,
+ "width": 437.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "repr(x) in your code",
+ "originalText": "repr(x) in your code",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "ef613f0c38fa2b12c09f",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "7d80be06a87afe7f5e42",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 180.0,
+ "width": 469.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#e8590c",
+ "backgroundColor": "#ffd8a8",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "a7519cf0e6761c6e13b1",
+ "type": "text"
+ },
+ {
+ "id": "8c30f71d3615b60941ee",
+ "type": "arrow"
+ },
+ {
+ "id": "f3c90ada8ccb525e284b",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "a7519cf0e6761c6e13b1",
+ "type": "text",
+ "x": 16.0,
+ "y": 202.5,
+ "width": 437.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "the C function reads Py_TYPE(x)->tp_repr",
+ "originalText": "the C function reads Py_TYPE(x)->tp_repr",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "7d80be06a87afe7f5e42",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "82cfcd3e2fa9b85c2452",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 310.0,
+ "width": 469.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "5bdadef92f02c0b28bff",
+ "type": "text"
+ },
+ {
+ "id": "f3c90ada8ccb525e284b",
+ "type": "arrow"
+ },
+ {
+ "id": "b5f94b1b79e74b4b8591",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "5bdadef92f02c0b28bff",
+ "type": "text",
+ "x": 16.0,
+ "y": 332.5,
+ "width": 437.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "which is slot_tp_repr for a Python class",
+ "originalText": "which is slot_tp_repr for a Python class",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "82cfcd3e2fa9b85c2452",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "803981049b37c28d2b28",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 440.0,
+ "width": 469.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#e8590c",
+ "backgroundColor": "#ffd8a8",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "8b62771efa1e2e0dfdb9",
+ "type": "text"
+ },
+ {
+ "id": "b5f94b1b79e74b4b8591",
+ "type": "arrow"
+ },
+ {
+ "id": "af2469fd3c375a7f9c8b",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "8b62771efa1e2e0dfdb9",
+ "type": "text",
+ "x": 16.0,
+ "y": 462.5,
+ "width": 437.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "that looks __repr__ up on the type",
+ "originalText": "that looks __repr__ up on the type",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "803981049b37c28d2b28",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "3c91f14cba951f82199b",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 570.0,
+ "width": 469.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#099268",
+ "backgroundColor": "#96f2d7",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "a55fe32d08aa5c8fa15d",
+ "type": "text"
+ },
+ {
+ "id": "af2469fd3c375a7f9c8b",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "a55fe32d08aa5c8fa15d",
+ "type": "text",
+ "x": 16.0,
+ "y": 592.5,
+ "width": 437.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "and calls it with x",
+ "originalText": "and calls it with x",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "3c91f14cba951f82199b",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "8c30f71d3615b60941ee",
+ "type": "arrow",
+ "x": 234.85000000000002,
+ "y": 120.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "ef613f0c38fa2b12c09f",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "7d80be06a87afe7f5e42",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "8caae22abe86168e3062",
+ "type": "text",
+ "x": 246.85000000000002,
+ "y": 140.0,
+ "width": 350.4,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "the instance dict is not consulted at all",
+ "originalText": "the instance dict is not consulted at all",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "f3c90ada8ccb525e284b",
+ "type": "arrow",
+ "x": 234.85000000000002,
+ "y": 250.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "7d80be06a87afe7f5e42",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "82cfcd3e2fa9b85c2452",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "b8f7a6da8d9a03eb068b",
+ "type": "text",
+ "x": 246.85000000000002,
+ "y": 270.0,
+ "width": 326.4,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "one dispatcher for every Python class",
+ "originalText": "one dispatcher for every Python class",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "b5f94b1b79e74b4b8591",
+ "type": "arrow",
+ "x": 234.85000000000002,
+ "y": 380.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "82cfcd3e2fa9b85c2452",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "803981049b37c28d2b28",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "2c5a08799a28d96fab68",
+ "type": "text",
+ "x": 246.85000000000002,
+ "y": 400.0,
+ "width": 295.2,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "on the type, and only on the type",
+ "originalText": "on the type, and only on the type",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "af2469fd3c375a7f9c8b",
+ "type": "arrow",
+ "x": 234.85000000000002,
+ "y": 510.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "803981049b37c28d2b28",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "3c91f14cba951f82199b",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
diff --git a/lessons/o03-dunders-and-slots/diagrams/looked-up-on-the-type.svg b/lessons/o03-dunders-and-slots/diagrams/looked-up-on-the-type.svg
new file mode 100644
index 0000000..51e2283
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/looked-up-on-the-type.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o03-dunders-and-slots/diagrams/one-name-many-slots.excalidraw b/lessons/o03-dunders-and-slots/diagrams/one-name-many-slots.excalidraw
new file mode 100644
index 0000000..cf0e25a
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/one-name-many-slots.excalidraw
@@ -0,0 +1,1035 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "6e6c9ff52f9b4cadcd5b",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 649.4399999999999,
+ "height": 30.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "The mapping is not one to one in either direction",
+ "originalText": "The mapping is not one to one in either direction",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "2320dffcfd58f6ecf945",
+ "type": "text",
+ "x": 14.0,
+ "y": 50.0,
+ "width": 131.51999999999998,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "what you write",
+ "originalText": "what you write",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "caf6ab6447889617d4a1",
+ "type": "text",
+ "x": 253.2,
+ "y": 50.0,
+ "width": 105.36,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "what it fills",
+ "originalText": "what it fills",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "f5dd7ee0231dec0640cd",
+ "type": "text",
+ "x": 511.59999999999997,
+ "y": 50.0,
+ "width": 206.15999999999997,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "and what that buys you",
+ "originalText": "and what that buys you",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "7aaa7adcff5fb4d62ca0",
+ "type": "line",
+ "x": 0.0,
+ "y": 76.0,
+ "width": 823.1999999999999,
+ "height": 0.0,
+ "angle": 0,
+ "strokeColor": "#ced4da",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "22e409b58990a3f5b063",
+ "type": "line",
+ "x": 0.0,
+ "y": 76.0,
+ "width": 823.1999999999999,
+ "height": 40.0,
+ "angle": 0,
+ "strokeColor": "transparent",
+ "backgroundColor": "#e9ecef",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "7c9c2de2319d561c5913",
+ "type": "text",
+ "x": 14.0,
+ "y": 86.0,
+ "width": 211.2,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "__eq__ and five others",
+ "originalText": "__eq__ and five others",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "f1af8cf896abcafd002c",
+ "type": "text",
+ "x": 253.2,
+ "y": 86.0,
+ "width": 134.4,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "tp_richcompare",
+ "originalText": "tp_richcompare",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "5467b1dd3aa11afa1a2f",
+ "type": "text",
+ "x": 511.59999999999997,
+ "y": 86.0,
+ "width": 288.0,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "one slot, an op argument picks",
+ "originalText": "one slot, an op argument picks",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "3706cdce407a410e847a",
+ "type": "line",
+ "x": 0.0,
+ "y": 116.0,
+ "width": 823.1999999999999,
+ "height": 0.0,
+ "angle": 0,
+ "strokeColor": "#ced4da",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "d717355aa1053c215f95",
+ "type": "line",
+ "x": 0.0,
+ "y": 116.0,
+ "width": 823.1999999999999,
+ "height": 40.0,
+ "angle": 0,
+ "strokeColor": "transparent",
+ "backgroundColor": "#ffd8a8",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "d4bd42f9b71fe69ed176",
+ "type": "text",
+ "x": 14.0,
+ "y": 126.0,
+ "width": 67.2,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "__len__",
+ "originalText": "__len__",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "d0f96180a18213450b9c",
+ "type": "text",
+ "x": 253.2,
+ "y": 126.0,
+ "width": 220.79999999999998,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "mp_length and sq_length",
+ "originalText": "mp_length and sq_length",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "c8fefb00d5361d87ba40",
+ "type": "text",
+ "x": 511.59999999999997,
+ "y": 126.0,
+ "width": 268.8,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "len and truthiness both work",
+ "originalText": "len and truthiness both work",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "d67baeeb0d7f5aef3a17",
+ "type": "line",
+ "x": 0.0,
+ "y": 156.0,
+ "width": 823.1999999999999,
+ "height": 0.0,
+ "angle": 0,
+ "strokeColor": "#ced4da",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "85cfa4d6fbe00c59d54c",
+ "type": "line",
+ "x": 0.0,
+ "y": 156.0,
+ "width": 823.1999999999999,
+ "height": 40.0,
+ "angle": 0,
+ "strokeColor": "transparent",
+ "backgroundColor": "#ffd8a8",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "8c6cf58cccfbb288a477",
+ "type": "text",
+ "x": 14.0,
+ "y": 166.0,
+ "width": 105.6,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "__getitem__",
+ "originalText": "__getitem__",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "85be4507866bb4bd20fb",
+ "type": "text",
+ "x": 253.2,
+ "y": 166.0,
+ "width": 230.39999999999998,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "mp_subscript and sq_item",
+ "originalText": "mp_subscript and sq_item",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "86e9d54c8d15a50f5933",
+ "type": "text",
+ "x": 511.59999999999997,
+ "y": 166.0,
+ "width": 288.0,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "and the class becomes iterable",
+ "originalText": "and the class becomes iterable",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "666a04b72a4bd0591b39",
+ "type": "line",
+ "x": 0.0,
+ "y": 196.0,
+ "width": 823.1999999999999,
+ "height": 0.0,
+ "angle": 0,
+ "strokeColor": "#ced4da",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "4816a99f4abf15165e09",
+ "type": "line",
+ "x": 0.0,
+ "y": 196.0,
+ "width": 823.1999999999999,
+ "height": 40.0,
+ "angle": 0,
+ "strokeColor": "transparent",
+ "backgroundColor": "#ffc9c9",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "3abf83eaae3e4b6682f4",
+ "type": "text",
+ "x": 14.0,
+ "y": 206.0,
+ "width": 192.0,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "__add__ and __radd__",
+ "originalText": "__add__ and __radd__",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "f8bda6a0ec5cac274f24",
+ "type": "text",
+ "x": 253.2,
+ "y": 206.0,
+ "width": 57.599999999999994,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "nb_add",
+ "originalText": "nb_add",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "25eb2fb305bc4d6807f4",
+ "type": "text",
+ "x": 511.59999999999997,
+ "y": 206.0,
+ "width": 297.59999999999997,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "one slot decides who goes first",
+ "originalText": "one slot decides who goes first",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "b8313aaa5512c39a0446",
+ "type": "line",
+ "x": 0.0,
+ "y": 236.0,
+ "width": 823.1999999999999,
+ "height": 0.0,
+ "angle": 0,
+ "strokeColor": "#ced4da",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0.0
+ ],
+ [
+ 823.1999999999999,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "868aab296c2a906dc870",
+ "type": "text",
+ "x": 0.0,
+ "y": 256.0,
+ "width": 556.3199999999999,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "Every surprise about dunder methods is one of these four rows.",
+ "originalText": "Every surprise about dunder methods is one of these four rows.",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
diff --git a/lessons/o03-dunders-and-slots/diagrams/one-name-many-slots.svg b/lessons/o03-dunders-and-slots/diagrams/one-name-many-slots.svg
new file mode 100644
index 0000000..bb963f9
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/one-name-many-slots.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o03-dunders-and-slots/diagrams/one-row-of-the-table.excalidraw b/lessons/o03-dunders-and-slots/diagrams/one-row-of-the-table.excalidraw
new file mode 100644
index 0000000..3ea6b9d
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/one-row-of-the-table.excalidraw
@@ -0,0 +1,473 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "3f0a0181bb5839c3c32f",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 411.48,
+ "height": 30.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "What one row of slotdefs holds",
+ "originalText": "What one row of slotdefs holds",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "764cd07e07a164f68113",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 50.0,
+ "width": 608.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#e8590c",
+ "backgroundColor": "#ffd8a8",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "5ab32918fd8e07c10eb3",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "5ab32918fd8e07c10eb3",
+ "type": "text",
+ "x": 16.0,
+ "y": 66.0,
+ "width": 576.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "the docstring that wrapper will carry",
+ "originalText": "the docstring that wrapper will carry",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "764cd07e07a164f68113",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "11cc25ac2a11ddbcb370",
+ "type": "text",
+ "x": 624.0,
+ "y": 66.0,
+ "width": 25.2,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "top",
+ "originalText": "top",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "32df9719e77222abf9e5",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 102.0,
+ "width": 608.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "320438f7019d52b86db0",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "320438f7019d52b86db0",
+ "type": "text",
+ "x": 16.0,
+ "y": 118.0,
+ "width": 576.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "the wrapper to expose when C defines it",
+ "originalText": "the wrapper to expose when C defines it",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "32df9719e77222abf9e5",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "744f6bf4f33c860385a8",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 154.0,
+ "width": 608.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "95af68e95a861bb9094b",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "95af68e95a861bb9094b",
+ "type": "text",
+ "x": 16.0,
+ "y": 170.0,
+ "width": 576.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "the dispatcher to install when Python defines it",
+ "originalText": "the dispatcher to install when Python defines it",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "744f6bf4f33c860385a8",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "27d3dfc0924ab31ae66c",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 206.0,
+ "width": 608.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "ff6a434a84e7cc8e50db",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "ff6a434a84e7cc8e50db",
+ "type": "text",
+ "x": 16.0,
+ "y": 222.0,
+ "width": 576.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "the offset of the slot inside PyHeapTypeObject",
+ "originalText": "the offset of the slot inside PyHeapTypeObject",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "27d3dfc0924ab31ae66c",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "3c4dbb02d6fa525bd0dd",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 258.0,
+ "width": 608.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "ea4ec7f16879301ffa55",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "ea4ec7f16879301ffa55",
+ "type": "text",
+ "x": 16.0,
+ "y": 274.0,
+ "width": 576.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "the name, as a string and as an interned object",
+ "originalText": "the name, as a string and as an interned object",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "3c4dbb02d6fa525bd0dd",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "96d2ca6b43cd2bf5c10b",
+ "type": "text",
+ "x": 0.0,
+ "y": 330.0,
+ "width": 709.92,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "The offset is why the table is sorted by slot rather than alphabetically by name.",
+ "originalText": "The offset is why the table is sorted by slot rather than alphabetically by name.",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
diff --git a/lessons/o03-dunders-and-slots/diagrams/one-row-of-the-table.svg b/lessons/o03-dunders-and-slots/diagrams/one-row-of-the-table.svg
new file mode 100644
index 0000000..760016b
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/one-row-of-the-table.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o03-dunders-and-slots/diagrams/the-table-runs-both-ways.excalidraw b/lessons/o03-dunders-and-slots/diagrams/the-table-runs-both-ways.excalidraw
new file mode 100644
index 0000000..8025418
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/the-table-runs-both-ways.excalidraw
@@ -0,0 +1,753 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "9e0ae64b186fe5d7d51e",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 1029.6,
+ "height": 30.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "One table, read forwards when you write a class and backwards when C does",
+ "originalText": "One table, read forwards when you write a class and backwards when C does",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "3228a58ed976cdf10293",
+ "type": "text",
+ "x": 67.9,
+ "y": 50.0,
+ "width": 328.2,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "you wrote the class in Python",
+ "originalText": "you wrote the class in Python",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "8a635bf5fb67e5177607",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 87.0,
+ "width": 464.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#1971c2",
+ "backgroundColor": "#a5d8ff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "e1fa988106eaa63789ad",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "e1fa988106eaa63789ad",
+ "type": "text",
+ "x": 16.0,
+ "y": 103.0,
+ "width": 432.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "your __repr__ goes in the class dict",
+ "originalText": "your __repr__ goes in the class dict",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "8a635bf5fb67e5177607",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "205a2cf28b22746ff23f",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 147.0,
+ "width": 464.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#495057",
+ "backgroundColor": "#e9ecef",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "7a7ad9c69c9ef954e668",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "7a7ad9c69c9ef954e668",
+ "type": "text",
+ "x": 16.0,
+ "y": 163.0,
+ "width": 432.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "fixup_slot_dispatchers reads it",
+ "originalText": "fixup_slot_dispatchers reads it",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "205a2cf28b22746ff23f",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "1c25b16e04fadd491750",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 207.0,
+ "width": 464.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#495057",
+ "backgroundColor": "#e9ecef",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "d3851c7c6450f3674ab6",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "d3851c7c6450f3674ab6",
+ "type": "text",
+ "x": 16.0,
+ "y": 223.0,
+ "width": 432.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "tp_repr gets a generic dispatcher",
+ "originalText": "tp_repr gets a generic dispatcher",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "1c25b16e04fadd491750",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "d9c487a55dfbc601edce",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 267.0,
+ "width": 464.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#495057",
+ "backgroundColor": "#e9ecef",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "8cbefce155418fb97a8a",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "8cbefce155418fb97a8a",
+ "type": "text",
+ "x": 16.0,
+ "y": 283.0,
+ "width": 432.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "which looks your function back up",
+ "originalText": "which looks your function back up",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "d9c487a55dfbc601edce",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "8246d4bb2439ef1bd99b",
+ "type": "text",
+ "x": 614.55,
+ "y": 50.0,
+ "width": 282.9,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "the type was written in C",
+ "originalText": "the type was written in C",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "ef20f81f98168bb4e7c5",
+ "type": "rectangle",
+ "x": 524.0,
+ "y": 87.0,
+ "width": 464.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#099268",
+ "backgroundColor": "#96f2d7",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "90e66fd50d88896f8660",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "90e66fd50d88896f8660",
+ "type": "text",
+ "x": 540.0,
+ "y": 103.0,
+ "width": 432.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "tp_repr already has a function",
+ "originalText": "tp_repr already has a function",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "ef20f81f98168bb4e7c5",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "3ab394b856f924ce81b0",
+ "type": "rectangle",
+ "x": 524.0,
+ "y": 147.0,
+ "width": 464.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#495057",
+ "backgroundColor": "#e9ecef",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "d3069e9cc5129a1170e8",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "d3069e9cc5129a1170e8",
+ "type": "text",
+ "x": 540.0,
+ "y": 163.0,
+ "width": 432.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "add_operators reads it",
+ "originalText": "add_operators reads it",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "3ab394b856f924ce81b0",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "1de9d08db8941410e12d",
+ "type": "rectangle",
+ "x": 524.0,
+ "y": 207.0,
+ "width": 464.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#495057",
+ "backgroundColor": "#e9ecef",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "0e2deee80756baa5dd89",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "0e2deee80756baa5dd89",
+ "type": "text",
+ "x": 540.0,
+ "y": 223.0,
+ "width": 432.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "__repr__ appears in the class dict",
+ "originalText": "__repr__ appears in the class dict",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "1de9d08db8941410e12d",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "1a3509a22bec4189a680",
+ "type": "rectangle",
+ "x": 524.0,
+ "y": 267.0,
+ "width": 464.0,
+ "height": 57.0,
+ "angle": 0,
+ "strokeColor": "#495057",
+ "backgroundColor": "#e9ecef",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "5cef359d760dcf17e61a",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "5cef359d760dcf17e61a",
+ "type": "text",
+ "x": 540.0,
+ "y": 283.0,
+ "width": 432.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "as a wrapper around the C one",
+ "originalText": "as a wrapper around the C one",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "1a3509a22bec4189a680",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "76bed5ef3912d8150842",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 344.0,
+ "width": 988.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#e8590c",
+ "backgroundColor": "#ffd8a8",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "c3c1a77e50dd081e2d1e",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "c3c1a77e50dd081e2d1e",
+ "type": "text",
+ "x": 16.0,
+ "y": 361.5,
+ "width": 956.0,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "Neither direction is a special case. Both walk the same ninety four rows.",
+ "originalText": "Neither direction is a special case. Both walk the same ninety four rows.",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "76bed5ef3912d8150842",
+ "lineHeight": 1.25,
+ "autoResize": false
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
diff --git a/lessons/o03-dunders-and-slots/diagrams/the-table-runs-both-ways.svg b/lessons/o03-dunders-and-slots/diagrams/the-table-runs-both-ways.svg
new file mode 100644
index 0000000..d3bede2
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/the-table-runs-both-ways.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o03-dunders-and-slots/diagrams/who-goes-first.excalidraw b/lessons/o03-dunders-and-slots/diagrams/who-goes-first.excalidraw
new file mode 100644
index 0000000..8c2761d
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/who-goes-first.excalidraw
@@ -0,0 +1,580 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "98bd0fba790f2651d936",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 695.8799999999999,
+ "height": 30.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "How one slot decides between __add__ and __radd__",
+ "originalText": "How one slot decides between __add__ and __radd__",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "cf094f2a89ab1adcc5eb",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 50.0,
+ "width": 484.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#1971c2",
+ "backgroundColor": "#a5d8ff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "84f6301adae696bb1a0b",
+ "type": "text"
+ },
+ {
+ "id": "71811f3fcc60591a6917",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "84f6301adae696bb1a0b",
+ "type": "text",
+ "x": 16.0,
+ "y": 72.5,
+ "width": 452.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "self + other, and nb_add dispatches both",
+ "originalText": "self + other, and nb_add dispatches both",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "cf094f2a89ab1adcc5eb",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "17a2c1dcfaefa7586b84",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 180.0,
+ "width": 484.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "1d06a959ac35921dbd00",
+ "type": "text"
+ },
+ {
+ "id": "71811f3fcc60591a6917",
+ "type": "arrow"
+ },
+ {
+ "id": "91b4ef2e93368aa760d2",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "1d06a959ac35921dbd00",
+ "type": "text",
+ "x": 16.0,
+ "y": 202.5,
+ "width": 452.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "is other's type a subclass of self's type",
+ "originalText": "is other's type a subclass of self's type",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "17a2c1dcfaefa7586b84",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "f08221a00a55f0d64a0f",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 310.0,
+ "width": 484.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "9d71d7052e4fcabb8063",
+ "type": "text"
+ },
+ {
+ "id": "91b4ef2e93368aa760d2",
+ "type": "arrow"
+ },
+ {
+ "id": "bd26138f23bde7e5fe6d",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "9d71d7052e4fcabb8063",
+ "type": "text",
+ "x": 16.0,
+ "y": 332.5,
+ "width": 452.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "and does other define __radd__ itself",
+ "originalText": "and does other define __radd__ itself",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "f08221a00a55f0d64a0f",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "81a173b8623b9dcbb69c",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 440.0,
+ "width": 484.70000000000005,
+ "height": 70.0,
+ "angle": 0,
+ "strokeColor": "#099268",
+ "backgroundColor": "#96f2d7",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": {
+ "type": 3
+ },
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [
+ {
+ "id": "43a140f6d8ad8cc5893c",
+ "type": "text"
+ },
+ {
+ "id": "bd26138f23bde7e5fe6d",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "43a140f6d8ad8cc5893c",
+ "type": "text",
+ "x": 16.0,
+ "y": 462.5,
+ "width": 452.70000000000005,
+ "height": 25.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "text": "if both, __radd__ first, otherwise __add__",
+ "originalText": "if both, __radd__ first, otherwise __add__",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "81a173b8623b9dcbb69c",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "71811f3fcc60591a6917",
+ "type": "arrow",
+ "x": 242.35000000000002,
+ "y": 120.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "cf094f2a89ab1adcc5eb",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "17a2c1dcfaefa7586b84",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "b68e08cc49e1e85b7fdd",
+ "type": "text",
+ "x": 254.35000000000002,
+ "y": 140.0,
+ "width": 362.16,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "so the reflected call is worth considering",
+ "originalText": "so the reflected call is worth considering",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "91b4ef2e93368aa760d2",
+ "type": "arrow",
+ "x": 242.35000000000002,
+ "y": 250.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "17a2c1dcfaefa7586b84",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "f08221a00a55f0d64a0f",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "c91532db8f4520fc9d8a",
+ "type": "text",
+ "x": 254.35000000000002,
+ "y": 270.0,
+ "width": 413.76,
+ "height": 20.0,
+ "angle": 0,
+ "strokeColor": "#5c5f66",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "customData": null,
+ "text": "inheriting it from a shared base does not count",
+ "originalText": "inheriting it from a shared base does not count",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "bd26138f23bde7e5fe6d",
+ "type": "arrow",
+ "x": 242.35000000000002,
+ "y": 380.0,
+ "width": 0.0,
+ "height": 60.0,
+ "angle": 0,
+ "strokeColor": "#1e1e1e",
+ "backgroundColor": "transparent",
+ "fillStyle": "solid",
+ "strokeWidth": 2,
+ "strokeStyle": "solid",
+ "roughness": 0,
+ "opacity": 100,
+ "groupIds": [],
+ "frameId": null,
+ "roundness": null,
+ "seed": 1,
+ "version": 1,
+ "versionNonce": 1,
+ "isDeleted": false,
+ "boundElements": [],
+ "updated": 1,
+ "link": null,
+ "locked": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.0,
+ 60.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "startBinding": {
+ "elementId": "f08221a00a55f0d64a0f",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "81a173b8623b9dcbb69c",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
diff --git a/lessons/o03-dunders-and-slots/diagrams/who-goes-first.svg b/lessons/o03-dunders-and-slots/diagrams/who-goes-first.svg
new file mode 100644
index 0000000..3085fd9
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/diagrams/who-goes-first.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o03-dunders-and-slots/o03.ipynb b/lessons/o03-dunders-and-slots/o03.ipynb
new file mode 100644
index 0000000..9765674
--- /dev/null
+++ b/lessons/o03-dunders-and-slots/o03.ipynb
@@ -0,0 +1,428 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "o03-01",
+ "metadata": {},
+ "source": [
+ "# O03. Dunders and the slots behind them\n",
+ "\n",
+ "[](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o03-dunders-and-slots/o03.ipynb)\n",
+ "\n",
+ "O02 opened the type struct and walked past the interesting part. In the middle of it are about seventy fields that are function pointers: `tp_repr`, `tp_hash`, `tp_call`, `tp_iter` and the rest. The interpreter reads those directly. When you write `repr(x)`, C code reaches into `Py_TYPE(x)->tp_repr` and calls whatever is there.\n",
+ "\n",
+ "But you never set `tp_repr`. You write `__repr__` and it works. Something in between knows those two names belong together, and that something is a table.\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-02",
+ "metadata": {},
+ "source": [
+ "## About the source references\n",
+ "\n",
+ "Now and then this lesson points at CPython's own source, like this: `Objects/typeobject.c:11584-11590@v3.15.0rc1#slotdefs`.\n",
+ "\n",
+ "Read it as three parts: the file, the lines, and the release those line numbers belong to. Sometimes there is a fourth part after a `#`, which is the name of the thing those lines are inside.\n",
+ "\n",
+ "Every reference is a link, and every one is checked against the pinned source on each change, so a stale reference fails the build instead of sending you somewhere wrong. You never have to read any of it. The references are there so you can go deeper when you want to, and so you can check that this lesson is not making things up.\n",
+ "\n",
+ "## Setup\n",
+ "\n",
+ "Colab does not come with the small package these lessons use, so the next cell installs it. If you are running this from a checkout of the repository it is already installed and the cell does nothing."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-03",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "\n",
+ "if sys.version_info < (3, 14):\n",
+ " print(\"This lesson needs CPython 3.14 or newer.\")\n",
+ " print(f\"This runtime is {sys.version.split()[0]}, and the cells below will not run on it.\")\n",
+ "else:\n",
+ " try:\n",
+ " import pyxray\n",
+ " except ImportError:\n",
+ " %pip install -q \"pyxray @ git+https://github.com/tamnd/cpython-internals@main#subdirectory=pyxray\"\n",
+ " import pyxray"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-04",
+ "metadata": {},
+ "source": [
+ "## Which Python is this\n",
+ "\n",
+ "Everything below was checked against the version this cell prints and against 3.14. The two agree on every line of output in this lesson."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-05",
+ "metadata": {
+ "cpython_internals": {
+ "differs": "This prints the interpreter you are on, so it is different for everybody."
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import pyxray\n",
+ "\n",
+ "pyxray.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-06",
+ "metadata": {},
+ "source": [
+ "## Ninety four rows\n",
+ "\n",
+ "The table is called `slotdefs`, [Objects/typeobject.c:11584-11590@v3.15.0rc1#slotdefs](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L11584-L11590), and each row is written with one of a dozen small macros, [Objects/typeobject.c:11546-11553@v3.15.0rc1#TPSLOT](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L11546-L11553). A row holds the dunder name, the byte offset of the slot inside `PyHeapTypeObject`, a function to install when Python defines the name, a wrapper to expose when C defines the slot, and a docstring.\n",
+ "\n",
+ "\n",
+ "\n",
+ "The table is sorted by that offset rather than by name, which is the first clue that it is meant to be walked rather than searched.\n",
+ "\n",
+ "Reading it backwards is a function called `add_operators`, [Objects/typeobject.c:12456-12470@v3.15.0rc1#add_operators](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L12456-L12470). It runs when a type is made ready, walks every row, and for each slot that has a function in it puts a [slot wrapper](https://github.com/tamnd/cpython-internals/blob/main/GLOSSARY.md#slot-wrapper) in the type's dict under the dunder name. That is where `int.__add__` comes from. Nobody wrote it. It is `nb_add` wrapped so Python can call it.\n",
+ "\n",
+ "You can see the result on `object`. Sort its attributes by what kind of thing they are and the slot wrappers stand out from the ordinary methods, which came from a different table entirely.\n",
+ "\n",
+ "the dunder methods on a builtin type are slot wrappers generated from its C slots, and they are a different kind of object from both the plain methods on the same type and the functions you write in a class body"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-07",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "kinds = {}\n",
+ "for name in dir(object):\n",
+ " kinds.setdefault(type(getattr(object, name)).__name__, []).append(name)\n",
+ "\n",
+ "for kind, found in sorted(kinds.items()):\n",
+ " print(f\" {kind:26} {len(found):>2} {' '.join(sorted(found)[:3])}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-08",
+ "metadata": {},
+ "source": [
+ "Three of those kinds matter here. A `wrapper_descriptor` came out of a slot. A `method_descriptor` came out of the type's ordinary method table, which is a different table with no slots in it. And a plain `function` is what you get when you write the method yourself in a class body.\n",
+ "\n",
+ "Line them up next to each other and the difference is one word of output."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-09",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class P:\n",
+ " def __repr__(self):\n",
+ " return \"p\"\n",
+ "\n",
+ "\n",
+ "for label, value in [\n",
+ " (\"object.__repr__\", object.__repr__),\n",
+ " (\"int.__add__\", int.__add__),\n",
+ " (\"P.__repr__\", P.__repr__),\n",
+ "]:\n",
+ " print(f\" {label:18} {type(value).__name__}\")\n",
+ "\n",
+ "print()\n",
+ "print(f\" object.__repr__.__objclass__ {object.__repr__.__objclass__}\")\n",
+ "print(\" so the wrapper remembers which type's slot it came out of\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-10",
+ "metadata": {},
+ "source": [
+ "## Why a dunder on an instance does nothing\n",
+ "\n",
+ "Now the other direction. When you write a class in Python, your `__repr__` goes into the class dict as an ordinary function. Then `fixup_slot_dispatchers` walks the same table, [Objects/typeobject.c:12129-12138@v3.15.0rc1#fixup_slot_dispatchers](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L12129-L12138), and for every row whose name is in your class dict it puts the row's dispatcher into the slot.\n",
+ "\n",
+ "For `__repr__` that dispatcher is `slot_tp_repr`, and it is three lines long, [Objects/typeobject.c:10524-10530@v3.15.0rc1#SLOT0](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L10524-L10530). All it does is look the dunder name up and call it.\n",
+ "\n",
+ "The important word is where it looks. It looks on the type.\n",
+ "\n",
+ "\n",
+ "\n",
+ "That is the whole explanation for a thing that catches everybody at least once. Setting `__repr__` on an instance puts it in the instance dict, where `repr()` will never look, because `repr()` goes through the slot and the slot goes to the type. The attribute is really there. You can call it by hand. The built in function still ignores it.\n",
+ "\n",
+ "a dunder assigned to an instance is a real attribute you can call by name, and the built in function that would use it never sees it, because the slot looks the name up on the type"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-11",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class Q:\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "q = Q()\n",
+ "q.__repr__ = lambda: \"set on the instance\"\n",
+ "\n",
+ "print(f\" it really is in the instance dict {'__repr__' in q.__dict__}\")\n",
+ "print(f\" calling it by name works {q.__repr__()}\")\n",
+ "print(f\" and repr(q) ignores it completely {repr(q).startswith('<')}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-12",
+ "metadata": {},
+ "source": [
+ "The same machinery runs the other way when you assign to the class instead. `type.__setattr__` calls `update_slot`, [Objects/typeobject.c:12086-12096@v3.15.0rc1#update_slot](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L12086-L12096), which finds every row of the table with that name and refills the matching slot, on the type and on all of its subclasses.\n",
+ "\n",
+ "So this is not something that happens once at class creation time. Assign `__repr__` to a class an hour later and `repr()` changes immediately, for that class and for everything that inherits from it. Delete it and the slot goes back to what it inherited.\n",
+ "\n",
+ "assigning a dunder to a class updates the slot at once, on that class and on every subclass of it, and deleting it puts the inherited behaviour back"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-13",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class R:\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "class Child(R):\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "print(f\" to start with repr is the default: {repr(R()).startswith('<')}\")\n",
+ "\n",
+ "R.__repr__ = lambda self: \"the one I just assigned\"\n",
+ "mine, inherited = repr(R()), repr(Child())\n",
+ "print(f\" after assigning {mine}\")\n",
+ "print(f\" and the subclass {inherited}\")\n",
+ "\n",
+ "del R.__repr__\n",
+ "print(f\" after deleting repr is the default again: {repr(R()).startswith('<')}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-14",
+ "metadata": {},
+ "source": [
+ "## One name, several slots\n",
+ "\n",
+ "The table is not a one to one mapping and the comment above it says so, [Objects/typeobject.c:11522-11531@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L11522-L11531). Several names can point at one slot, and one name can appear in several rows.\n",
+ "\n",
+ "\n",
+ "\n",
+ "`__len__` is the easy case. It appears twice, once as `mp_length` and once as `sq_length`, [Objects/typeobject.c:11740-11753@v3.15.0rc1#MPSLOT](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L11740-L11753), so defining it once fills both. That is why a class with only a `__len__` gets truthiness for free: `bool()` has no `__bool__` to call, so it falls back on the length.\n",
+ "\n",
+ "`__getitem__` is the fun one. It fills `mp_subscript` and `sq_item`, and `sq_item` is the old sequence protocol, the one that predates iterators. Anything that wants to iterate will use it if there is no `__iter__`, asking for item 0, item 1, item 2 and so on until it gets an `IndexError`. So a class with a single `__getitem__` and nothing else is iterable, and supports `in`, without either of the dunders you would expect to need.\n",
+ "\n",
+ "a class defining only __getitem__ can be iterated over and used with in, because that one name fills the sq_item slot and the old sequence protocol is still what iteration falls back on"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-15",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class Countdown:\n",
+ " def __getitem__(self, index):\n",
+ " if index > 4:\n",
+ " raise IndexError(index)\n",
+ " return 10 - index\n",
+ "\n",
+ "\n",
+ "it = Countdown()\n",
+ "\n",
+ "print(f\" Countdown has no __iter__ {'__iter__' not in dir(Countdown)}\")\n",
+ "print(f\" and no __contains__ {'__contains__' not in dir(Countdown)}\")\n",
+ "print(f\" and yet list(it) is {list(it)}\")\n",
+ "print(f\" and 7 in it is {7 in it}\")\n",
+ "\n",
+ "\n",
+ "class Sized:\n",
+ " def __len__(self):\n",
+ " return 3\n",
+ "\n",
+ "\n",
+ "print()\n",
+ "print(f\" Sized has no __bool__ {'__bool__' not in dir(Sized)}\")\n",
+ "print(f\" and bool(Sized()) is {bool(Sized())}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-16",
+ "metadata": {},
+ "source": [
+ "## One slot, two names\n",
+ "\n",
+ "The other direction is more interesting, because when two names share a slot the slot has to decide between them at call time.\n",
+ "\n",
+ "`__add__` and `__radd__` both fill `nb_add`. There is only one pointer, so `a + b` cannot dispatch on which method exists. Instead the dispatcher is generated by a macro that has the whole decision written into it, [Objects/typeobject.c:10573-10600@v3.15.0rc1#SLOT1BINFULL](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L10573-L10600), and it asks two questions before it will consider the reflected call.\n",
+ "\n",
+ "\n",
+ "\n",
+ "First, is the right hand type a subclass of the left hand type. Second, does the right hand type actually define `__radd__` itself rather than inheriting it. Only if both are true does `__radd__` go first. Otherwise `__add__` runs, and `__radd__` gets its ordinary turn afterwards if `__add__` returns `NotImplemented`.\n",
+ "\n",
+ "The second question is the one worth remembering. The macro calls `method_is_overloaded`, and inheriting `__radd__` from a shared base does not count as overloading it.\n",
+ "\n",
+ "a subclass that defines __radd__ itself is called before the base class __add__, and an unrelated class defining __radd__ is not, because both names share one slot and the dispatcher checks the subclass relationship first"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-17",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class Money:\n",
+ " def __add__(self, other):\n",
+ " return \"Money.__add__ ran\"\n",
+ "\n",
+ "\n",
+ "class Extra(Money):\n",
+ " def __radd__(self, other):\n",
+ " return \"Extra.__radd__ ran\"\n",
+ "\n",
+ "\n",
+ "class Other:\n",
+ " def __radd__(self, other):\n",
+ " return \"Other.__radd__ ran\"\n",
+ "\n",
+ "\n",
+ "print(f\" Money() + Other() {Money() + Other()}\")\n",
+ "print(f\" Money() + Money() {Money() + Money()}\")\n",
+ "print(f\" Money() + Extra() {Money() + Extra()}\")\n",
+ "print()\n",
+ "print(\" the third line is the odd one, and it is odd on purpose:\")\n",
+ "print(\" a subclass gets to answer for itself before its base does\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-18",
+ "metadata": {},
+ "source": [
+ "## Why defining __eq__ takes __hash__ away\n",
+ "\n",
+ "This one usually gets taught as a rule to memorise. It is not a rule, it is two pieces of slot machinery meeting.\n",
+ "\n",
+ "\n",
+ "\n",
+ "When a new type inherits its slots from its base, the comparison slots get special treatment, and the question asked is `overrides_hash`, [Objects/typeobject.c:8808-8818@v3.15.0rc1#overrides_hash](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L8808-L8818). It looks for `__eq__` in the class dict, and if it does not find that it looks for `__hash__`. If either is there, neither `tp_richcompare` nor `tp_hash` is copied down from the base.\n",
+ "\n",
+ "Then, later in making the type ready, `type_ready_set_hash` finds `tp_hash` still empty and no `__hash__` in the dict, [Objects/typeobject.c:9370-9391@v3.15.0rc1#type_ready_set_hash](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/typeobject.c#L9370-L9391). So it sets the slot to `PyObject_HashNotImplemented` and writes a real `None` into the class dict.\n",
+ "\n",
+ "That last part is why the effect is so visible. Your class does not merely fail to inherit a hash. It has an actual `__hash__` attribute whose value is `None`, put there by C code, and you can look at it.\n",
+ "\n",
+ "a class that defines __eq__ and not __hash__ ends up with a real None stored under __hash__ in its class dict, and putting a hash back is a one line assignment"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o03-19",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class Same:\n",
+ " def __eq__(self, other):\n",
+ " return True\n",
+ "\n",
+ "\n",
+ "class SameHashed:\n",
+ " def __eq__(self, other):\n",
+ " return True\n",
+ "\n",
+ " __hash__ = object.__hash__\n",
+ "\n",
+ "\n",
+ "print(f\" Same.__hash__ is {Same.__hash__}\")\n",
+ "print(f\" and it is really in the dict: {'__hash__' in Same.__dict__}\")\n",
+ "\n",
+ "try:\n",
+ " hash(Same())\n",
+ "except TypeError as error:\n",
+ " print(f\" hash(Same()) raises TypeError: {error}\")\n",
+ "\n",
+ "print()\n",
+ "print(f\" SameHashed put one back, and it works: {isinstance(hash(SameHashed()), int)}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o03-20",
+ "metadata": {},
+ "source": [
+ "## Try it yourself\n",
+ "\n",
+ "Three things to poke at.\n",
+ "\n",
+ "Find every name that gives you a slot wrapper on some builtin type but a plain function on a class you write. Walk `dir(list)` and `dir(str)`, check the type of each attribute, and you have most of the table's left hand column without ever opening the C file.\n",
+ "\n",
+ "Define a class with `__getitem__` that never raises `IndexError`, then call `list()` on it and be ready to interrupt. The old sequence protocol has no idea how long anything is, and the only thing that stops it is the exception. This is why `__iter__` was worth adding.\n",
+ "\n",
+ "Take the `Money` and `Extra` example and make `Extra` inherit `__radd__` from a shared base instead of defining it. The answer changes, because the dispatcher asks whether the subclass defines the method itself.\n",
+ "\n",
+ "## What just happened\n",
+ "\n",
+ "There are about seventy function pointer slots in a type object and a table of ninety four rows connecting them to dunder names. Everything in this lesson is that table being read in one direction or the other.\n",
+ "\n",
+ "Backwards, when a C type is made ready: `add_operators` turns each filled slot into a slot wrapper in the class dict, which is where `int.__add__` and `object.__repr__` come from.\n",
+ "\n",
+ "Forwards, when you write a class: `fixup_slot_dispatchers` puts a generic dispatcher into each slot whose name you defined. That dispatcher looks the name up on the type every time it runs, which is why a dunder set on an instance is ignored and why assigning one to a class takes effect at once, on that class and on its subclasses.\n",
+ "\n",
+ "The mapping is not one to one either way. `__len__` fills two slots, so it gives you truthiness. `__getitem__` fills two slots, so it gives you iteration and `in`. And `__add__` and `__radd__` share one slot, so the dispatcher has the subclass rule baked into it.\n",
+ "\n",
+ "The `__eq__` and `__hash__` link is two mechanisms meeting: `overrides_hash` stops the slot being inherited, and `type_ready_set_hash` writes a literal `None` in its place.\n",
+ "\n",
+ "## What is next\n",
+ "\n",
+ "O04 is the method resolution order. Everything in this lesson assumed there was one obvious base class to inherit a slot from. With more than one base there is not, and CPython uses an algorithm with a name and a paper behind it to put the bases in an order that makes inheritance mean something. It also refuses to build the class at all when no such order exists, and seeing that failure is the fastest way to understand what the algorithm is protecting."
+ ]
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "provenance": []
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/probes/pyodide/lessons.json b/probes/pyodide/lessons.json
index 8dadacb..ad41828 100644
--- a/probes/pyodide/lessons.json
+++ b/probes/pyodide/lessons.json
@@ -70,7 +70,7 @@
{
"name": "b03-07",
"status": "ok",
- "printed": "test_adding (__main__.Arithmetic.test_adding) ... ok\ntest_dividing (__main__.Arithmetic.test_dividing) ... ok\ntest_powers (__main__.Arithmetic.test_powers) ... FAIL\n\n======================================================================\nFAIL: test_powers (__main__.Arithmetic.test_powers)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"b03-07\", line 13, in test_powers\nAssertionError: 1024 != 1000\n\n----------------------------------------------------------------------\nRan 3 tests in 0.001s\n\nFAILED (failures=1)\n\n3 tests run, 1 failed, 0 errored\n"
+ "printed": "test_adding (__main__.Arithmetic.test_adding) ... ok\ntest_dividing (__main__.Arithmetic.test_dividing) ... ok\ntest_powers (__main__.Arithmetic.test_powers) ... FAIL\n\n======================================================================\nFAIL: test_powers (__main__.Arithmetic.test_powers)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"b03-07\", line 13, in test_powers\nAssertionError: 1024 != 1000\n\n----------------------------------------------------------------------\nRan 3 tests in 0.002s\n\nFAILED (failures=1)\n\n3 tests run, 1 failed, 0 errored\n"
},
{
"name": "b03-10",
@@ -539,7 +539,7 @@
{
"name": "f10-11",
"status": "ok",
- "printed": " -- MAKE_CELL 0 (a)\n MAKE_CELL 6 (n)\n\n 1 RESUME 0\n\n 2 LOAD_SMALL_INT 0\n STORE_DEREF 6 (n)\n\n 4 LOAD_FAST_BORROW 0 (a)\n LOAD_FAST_BORROW 6 (n)\n BUILD_TUPLE 2\n LOAD_CONST 1 ()\n MAKE_FUNCTION\n SET_FUNCTION_ATTRIBUTE 8 (closure)\n STORE_FAST 5 (inner)\n\n 7 LOAD_FAST_BORROW 5 (inner)\n RETURN_VALUE\n\nDisassembly of :\n -- COPY_FREE_VARS 2\n\n 4 RESUME 0\n\n 5 LOAD_DEREF 0 (a)\n LOAD_DEREF 1 (n)\n BINARY_OP 0 (+)\n RETURN_VALUE\n"
+ "printed": " -- MAKE_CELL 0 (a)\n MAKE_CELL 6 (n)\n\n 1 RESUME 0\n\n 2 LOAD_SMALL_INT 0\n STORE_DEREF 6 (n)\n\n 4 LOAD_FAST_BORROW 0 (a)\n LOAD_FAST_BORROW 6 (n)\n BUILD_TUPLE 2\n LOAD_CONST 1 ()\n MAKE_FUNCTION\n SET_FUNCTION_ATTRIBUTE 8 (closure)\n STORE_FAST 5 (inner)\n\n 7 LOAD_FAST_BORROW 5 (inner)\n RETURN_VALUE\n\nDisassembly of :\n -- COPY_FREE_VARS 2\n\n 4 RESUME 0\n\n 5 LOAD_DEREF 0 (a)\n LOAD_DEREF 1 (n)\n BINARY_OP 0 (+)\n RETURN_VALUE\n"
},
{
"name": "f10-13",
@@ -631,12 +631,12 @@
{
"name": "f12-07",
"status": "ok",
- "printed": " greet.py is 54 bytes of text\n greet.cpython-314.pyc is 251 bytes\n\n header 2b 0e 0d 0a 00 00 00 00 c0 77 95 6a 36 00 00 00\n the rest 235 bytes, and marshal.loads gives back a\n code object\n"
+ "printed": " greet.py is 54 bytes of text\n greet.cpython-314.pyc is 251 bytes\n\n header 2b 0e 0d 0a 00 00 00 00 c3 a5 95 6a 36 00 00 00\n the rest 235 bytes, and marshal.loads gives back a\n code object\n"
},
{
"name": "f12-10",
"status": "ok",
- "printed": " magic 168627755\n flags 0\n mtime 1788180416 source 1788180416\n size 54 source 54\n"
+ "printed": " magic 168627755\n flags 0\n mtime 1788192195 source 1788192195\n size 54 source 54\n"
},
{
"name": "f12-13",
@@ -670,7 +670,7 @@
{
"name": "f12-29",
"status": "ok",
- "printed": " one bit flipped in the magic: bad magic number in 'broken': b'*\\x0e\\r\\n'\n\n put a wrong mtime in greet.cpython-314.pyc: 1788180287\n imported it with no complaint: 'hi world'\n and the header now says 1788180287\n the file was quietly rewritten: False\n"
+ "printed": " one bit flipped in the magic: bad magic number in 'broken': b'*\\x0e\\r\\n'\n\n put a wrong mtime in greet.cpython-314.pyc: 1788192060\n imported it with no complaint: 'hi world'\n and the header now says 1788192060\n the file was quietly rewritten: False\n"
}
]
},
@@ -689,7 +689,7 @@
{
"name": "o01-07",
"status": "ok",
- "printed": " a pointer here is 4 bytes, so the header is 8\n it lives at 24910176\n\n first word, read as a count 1\n sys.getrefcount, less its own argument 1\n the same number: True\n\n second word 24728248\n id(Thing) is 24728248\n so the second word is the type pointer: True\n\n after a second name, the count reads 2\n after deleting that name, it reads 1\n"
+ "printed": " a pointer here is 4 bytes, so the header is 8\n it lives at 22337768\n\n first word, read as a count 1\n sys.getrefcount, less its own argument 1\n the same number: True\n\n second word 24569584\n id(Thing) is 24569584\n so the second word is the type pointer: True\n\n after a second name, the count reads 2\n after deleting that name, it reads 1\n"
},
{
"name": "o01-10",
@@ -767,6 +767,55 @@
}
]
},
+ {
+ "slug": "o03-dunders-and-slots",
+ "cells": [
+ {
+ "name": "o03-03",
+ "status": "ok"
+ },
+ {
+ "name": "o03-05",
+ "status": "ok",
+ "printed": "cpython 3.14.2 on Emscripten wasm32, WebAssembly\n note: this is 3.14, and everything here is written against 3.15, so bytecode and some object layouts will differ from the prose\n"
+ },
+ {
+ "name": "o03-07",
+ "status": "ok",
+ "printed": " builtin_function_or_method 3 __init_subclass__ __new__ __subclasshook__\n method_descriptor 6 __dir__ __format__ __getstate__\n str 1 __doc__\n type 1 __class__\n wrapper_descriptor 13 __delattr__ __eq__ __ge__\n"
+ },
+ {
+ "name": "o03-09",
+ "status": "ok",
+ "printed": " object.__repr__ wrapper_descriptor\n int.__add__ wrapper_descriptor\n P.__repr__ function\n\n object.__repr__.__objclass__ \n so the wrapper remembers which type's slot it came out of\n"
+ },
+ {
+ "name": "o03-11",
+ "status": "ok",
+ "printed": " it really is in the instance dict True\n calling it by name works set on the instance\n and repr(q) ignores it completely True\n"
+ },
+ {
+ "name": "o03-13",
+ "status": "ok",
+ "printed": " to start with repr is the default: True\n after assigning the one I just assigned\n and the subclass the one I just assigned\n after deleting repr is the default again: True\n"
+ },
+ {
+ "name": "o03-15",
+ "status": "ok",
+ "printed": " Countdown has no __iter__ True\n and no __contains__ True\n and yet list(it) is [10, 9, 8, 7, 6]\n and 7 in it is True\n\n Sized has no __bool__ True\n and bool(Sized()) is True\n"
+ },
+ {
+ "name": "o03-17",
+ "status": "ok",
+ "printed": " Money() + Other() Money.__add__ ran\n Money() + Money() Money.__add__ ran\n Money() + Extra() Extra.__radd__ ran\n\n the third line is the odd one, and it is odd on purpose:\n a subclass gets to answer for itself before its base does\n"
+ },
+ {
+ "name": "o03-19",
+ "status": "ok",
+ "printed": " Same.__hash__ is None\n and it is really in the dict: True\n hash(Same()) raises TypeError: unhashable type: 'Same'\n\n SameHashed put one back, and it works: True\n"
+ }
+ ]
+ },
{
"slug": "t01-one-line-seven-stages",
"cells": [
@@ -1149,7 +1198,7 @@
{
"name": "t04-30",
"status": "ok",
- "printed": "free variables: ('total',)\nthe cells: (,)\nfirst call: 1\nsecond call: 2\nthe cell now: 2\n"
+ "printed": "free variables: ('total',)\nthe cells: (| ,)\nfirst call: 1\nsecond call: 2\nthe cell now: 2\n"
},
{
"name": "t04-33",
@@ -1426,7 +1475,7 @@
{
"name": "t07-38",
"status": "ok",
- "printed": "asked twice, got the same object: True\nand it is still here after the call returned: \nits name: make_one\n"
+ "printed": "asked twice, got the same object: True\nand it is still here after the call returned: \nits name: make_one\n"
},
{
"name": "t07-40",
@@ -1460,12 +1509,12 @@
{
"name": "t08-11",
"status": "ok",
- "printed": "NoneType at 0x30d7d8, refcount is parked, this object is never freed, 8 bytes, not tracked\nint at 0x31a7cc, refcount is parked, this object is never freed, 16 bytes, not tracked\nstr at 0x1852bc8, 3 reference(s), 26 bytes, not tracked\nlist at 0x177d798, 2 reference(s), 44 bytes, tracked by the cycle collector\ndict at 0x1563490, 2 reference(s), 108 bytes, tracked by the cycle collector\nfunction at 0x1196758, 4 reference(s), 84 bytes, tracked by the cycle collector\n"
+ "printed": "NoneType at 0x30d7d8, refcount is parked, this object is never freed, 8 bytes, not tracked\nint at 0x31a7cc, refcount is parked, this object is never freed, 16 bytes, not tracked\nstr at 0x1652458, 3 reference(s), 26 bytes, not tracked\nlist at 0x154e8b8, 2 reference(s), 44 bytes, tracked by the cycle collector\ndict at 0x177da80, 2 reference(s), 108 bytes, tracked by the cycle collector\nfunction at 0x13f69a8, 4 reference(s), 84 bytes, tracked by the cycle collector\n"
},
{
"name": "t08-14",
"status": "ok",
- "printed": "a == b True same contents\na is b False different objects\na is c True same object\n\nid(a) 0x1426dd8\nid(b) 0x17c30b8\nid(c) 0x1426dd8\n"
+ "printed": "a == b True same contents\na is b False different objects\na is c True same object\n\nid(a) 0x14fc2e0\nid(b) 0x178d2d0\nid(c) 0x14fc2e0\n"
},
{
"name": "t08-17",
@@ -1505,7 +1554,7 @@
{
"name": "t08-35",
"status": "ok",
- "printed": "dict at 0x10bca88\ndict at 0x139f820\nlist at 0x17c2fa0\n"
+ "printed": "dict at 0x12c16e8\ndict at 0x13ce7b0\nlist at 0x178d1b8\n"
},
{
"name": "t08-37",
@@ -1564,7 +1613,7 @@
{
"name": "t09-21",
"status": "ok",
- "printed": "Node at 0x11956b0 -> Node at 0x17cb9d0 -> Node at 0x14fd210 -> Node at 0x11956b0\n"
+ "printed": "Node at 0x149cb98 -> Node at 0x13951e8 -> Node at 0x181f098 -> Node at 0x149cb98\n"
},
{
"name": "t09-23",
@@ -1594,7 +1643,7 @@
{
"name": "t09-37",
"status": "ok",
- "printed": "first object was at 0x18c42a0\nsecond object is at 0x18c42a0\nsame address reused -> True\n"
+ "printed": "first object was at 0x178e3a8\nsecond object is at 0x178e3a8\nsame address reused -> True\n"
}
]
},
diff --git a/probes/pyodide/lessons.md b/probes/pyodide/lessons.md
index 4439c4f..122b450 100644
--- a/probes/pyodide/lessons.md
+++ b/probes/pyodide/lessons.md
@@ -2,7 +2,7 @@
Generated by `just build-probe`. Do not edit by hand, the change will be overwritten.
-30 lesson(s) on Pyodide 3.14.2: 30 ran end to end, 319 cell(s) in total.
+31 lesson(s) on Pyodide 3.14.2: 31 ran end to end, 328 cell(s) in total.
The checks in `report.md` next to this ask whether a surface exists. This runs the lessons themselves: every code cell of every notebook, in order, in one Pyodide runtime, with `pyxray` mounted off the disk rather than installed. The install cell is the one thing changed, and only its `%pip` line, which a reader in a browser does not need either.
@@ -26,6 +26,7 @@ The checks in `report.md` next to this ask whether a surface exists. This runs t
| f12-what-ends-up-on-disk | 11 | runs end to end |
| o01-the-header-byte-by-byte | 8 | runs end to end |
| o02-following-the-type-pointer | 9 | runs end to end |
+| o03-dunders-and-slots | 9 | runs end to end |
| t01-one-line-seven-stages | 18 | runs end to end |
| t02-text-becomes-tokens | 32 | runs end to end |
| t03-tokens-become-a-tree | 13 | runs end to end |
diff --git a/pyxray/src/pyxray/glossary.py b/pyxray/src/pyxray/glossary.py
index 3b71b7a..6218030 100644
--- a/pyxray/src/pyxray/glossary.py
+++ b/pyxray/src/pyxray/glossary.py
@@ -745,6 +745,24 @@ def anchor(name: str) -> str:
see=("object header", "object"),
met="O01",
),
+ Term(
+ name="slot",
+ short="One of the function pointer fields in a type object.",
+ long="`tp_repr`, `tp_hash`, `tp_call` and about seventy others. The interpreter reads them directly, so calling `repr(x)` is a load and an indirect call rather than a dictionary lookup. Python code never assigns to one. It defines a dunder method and a table walk fills the slot in, which is what makes the two spellings feel like the same thing.",
+ cite="Objects/typeobject.c:11584-11590@v3.15.0rc1#slotdefs",
+ also=("type slot",),
+ see=("slot wrapper", "type object"),
+ met="O03",
+ ),
+ Term(
+ name="slot wrapper",
+ short="A dunder method that is really a C slot with a Python callable wrapped around it.",
+ long="When a type written in C is made ready, `add_operators` walks the slot table and puts one of these into the class dict for every slot that has a function in it. That is where `int.__add__` and `object.__repr__` come from: nobody wrote them as methods, they are `nb_add` and `tp_repr` made callable. `type(int.__add__).__name__` is `wrapper_descriptor`, which is how you tell one from an ordinary method.",
+ cite="Objects/typeobject.c:12456-12470@v3.15.0rc1#add_operators",
+ also=("`wrapper_descriptor`",),
+ see=("slot", "type object"),
+ met="O03",
+ ),
Term(
name="static type",
short="A type object written out as a C literal and compiled into the binary.",
| |