From 1128197fec7c7bbf9e5319da27f41316d4063d00 Mon Sep 17 00:00:00 2001
From: tamnd <1218621+tamnd@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:09:30 +0700
Subject: [PATCH] O07: two arrays and a hash
---
GLOSSARY.md | 18 +-
README.md | 1 +
citations.lock.json | 80 +
lessons/CLAIMS.md | 11 +-
lessons/README.md | 1 +
lessons/o07-two-arrays-and-a-hash/build.py | 425 +++++
lessons/o07-two-arrays-and-a-hash/diagrams.py | 130 ++
.../diagrams/delete-leaves-a-mark.excalidraw | 613 ++++++
.../diagrams/delete-leaves-a-mark.svg | 1 +
.../diagrams/the-probe-recurrence.excalidraw | 403 ++++
.../diagrams/the-probe-recurrence.svg | 1 +
.../diagrams/two-arrays-not-one.excalidraw | 1694 +++++++++++++++++
.../diagrams/two-arrays-not-one.svg | 1 +
.../diagrams/two-kinds-of-entry.excalidraw | 746 ++++++++
.../diagrams/two-kinds-of-entry.svg | 1 +
.../diagrams/when-it-grows.excalidraw | 711 +++++++
.../diagrams/when-it-grows.svg | 1 +
.../diagrams/where-a-key-lands.excalidraw | 746 ++++++++
.../diagrams/where-a-key-lands.svg | 1 +
lessons/o07-two-arrays-and-a-hash/o07.ipynb | 528 +++++
probes/pyodide/lessons.json | 77 +-
probes/pyodide/lessons.md | 3 +-
pyxray/src/pyxray/glossary.py | 17 +
23 files changed, 6195 insertions(+), 15 deletions(-)
create mode 100644 lessons/o07-two-arrays-and-a-hash/build.py
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams.py
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/delete-leaves-a-mark.excalidraw
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/delete-leaves-a-mark.svg
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/the-probe-recurrence.excalidraw
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/the-probe-recurrence.svg
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/two-arrays-not-one.excalidraw
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/two-arrays-not-one.svg
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/two-kinds-of-entry.excalidraw
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/two-kinds-of-entry.svg
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/when-it-grows.excalidraw
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/when-it-grows.svg
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/where-a-key-lands.excalidraw
create mode 100644 lessons/o07-two-arrays-and-a-hash/diagrams/where-a-key-lands.svg
create mode 100644 lessons/o07-two-arrays-and-a-hash/o07.ipynb
diff --git a/GLOSSARY.md b/GLOSSARY.md
index 16c35f0..c98782c 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) | [C3 linearization](#c3-linearization) | [EXTENDED_ARG](#extended_arg) | [GC pre header](#gc-pre-header) | [JIT](#jit) | [MRO](#mro) | [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) | [bound method](#bound-method) | [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) | [data descriptor](#data-descriptor) | [deallocation](#deallocation) | [debug build](#debug-build) | [deoptimization](#deoptimization) | [descriptor](#descriptor) | [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)
+[ASDL](#asdl) | [Argument Clinic](#argument-clinic) | [C3 linearization](#c3-linearization) | [EXTENDED_ARG](#extended_arg) | [GC pre header](#gc-pre-header) | [JIT](#jit) | [MRO](#mro) | [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) | [bound method](#bound-method) | [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) | [compact dict](#compact-dict) | [computed goto](#computed-goto) | [configure](#configure) | [constant folding](#constant-folding) | [control flow graph](#control-flow-graph) | [cycle collector](#cycle-collector) | [data descriptor](#data-descriptor) | [deallocation](#deallocation) | [debug build](#debug-build) | [deoptimization](#deoptimization) | [descriptor](#descriptor) | [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) | [probe sequence](#probe-sequence) | [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
@@ -722,6 +722,22 @@ A class statement compiles to a call to `__build_class__`, which runs the class
First met in O02. See also [type object](#type-object), [heap type](#heap-type). In the source: [`Python/bltinmodule.c:102-108@v3.15.0rc1#builtin___build_class__`](https://github.com/python/cpython/blob/v3.15.0rc1/Python/bltinmodule.c#L102-L108).
+### compact dict
+
+**A dict laid out as a small array of slot numbers in front of an entry array.**
+
+The slot array, `dk_indices`, holds a row number into the entry array, or -1 for a position never used, or -2 for one that used to hold something. The entry array is appended to and never reordered, so iterating it top to bottom gives insertion order with no bookkeeping at all. Only the small array has holes, which is why the layout is called compact: a mostly empty hash table costs one byte per slot rather than a whole row.
+
+Also written compact ordered dict. First met in O07. See also [probe sequence](#probe-sequence), [instance dictionary](#instance-dictionary). In the source: [`Include/internal/pycore_dict.h:196-235@v3.15.0rc1#_dictkeysobject`](https://github.com/python/cpython/blob/v3.15.0rc1/Include/internal/pycore_dict.h#L196-L235).
+
+### probe sequence
+
+**The order of slots a dict lookup visits when the first one is a collision.**
+
+The first slot is the hash masked down to the table size. After that the step is `i = mask & (i * 5 + perturb + 1)`, where `perturb` starts as the whole hash and is shifted right by 5 each round. The `i * 5 + 1` part visits every slot exactly once and in an order unrelated to how consecutive keys arrive, and `perturb` brings back the high bits of the hash the mask discarded. For a size 8 table starting at slot 0 the order is 0, 1, 6, 7, 4, 5, 2, 3.
+
+First met in O07. See also [compact dict](#compact-dict). In the source: [`Objects/dictobject.c:1078-1101@v3.15.0rc1#do_lookup`](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L1078-L1101).
+
## Memory
Where objects come from and what happens to them afterwards. T09 is the lesson.
diff --git a/README.md b/README.md
index 8f1bdbd..935a49f 100644
--- a/README.md
+++ b/README.md
@@ -122,6 +122,7 @@ A lesson can also end with a boss fight, which is a problem the text does not so
| O04 | [The order things are found in](lessons/o04-the-order-things-are-found-in/o04.ipynb) | Why a class with two bases carries a flat list instead of searching, the twenty line merge that builds it and matches CPython exactly on classes out of the standard library, the two ways a class statement can fail before the class exists, why super goes somewhere the class it is written in has never heard of, and the two ways the list changes after the fact | M4 | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o04-the-order-things-are-found-in/o04.ipynb) |
| O05 | [What a dot does](lessons/o05-what-a-dot-does/o05.ipynb) | The four places x.name looks and the fixed order it looks in them, why a property cannot be shadowed by an instance attribute and a method can, why __getattr__ and __getattribute__ are not two versions of the same hook, why a class lookup starts at the metaclass and passes None where the instance would go, and the version tag that lets four thousand cache entries go stale without anybody clearing them | M4 | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o05-what-a-dot-does/o05.ipynb) |
| O06 | [What property actually is](lessons/o06-what-property-actually-is/o06.ipynb) | The three method protocol that property, classmethod, staticmethod and the self in every method you have written all turn out to be, the one line of C that decides whether a descriptor beats the instance dict or loses to it, the seven lines that build a bound method, the five descriptor types C code makes because it cannot write a class body, why a descriptor in an instance dict does nothing at all, and the pass over the class dict that tells each descriptor the name it was assigned to | M4 | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o06-what-property-actually-is/o06.ipynb) |
+| O07 | [Two arrays and a hash](lessons/o07-two-arrays-and-a-hash/o07.ipynb) | The small array of slot numbers in front of the append only array of entries, why insertion order comes free out of that split, the three lines that decide where a lookup looks next and the slot order they produce, the sixty lines of Python that grow at exactly the same key counts as the real dict, why deleting frees nothing and can even make a dict bigger, and why a dict of strings does not store its hashes | M4 | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o07-two-arrays-and-a-hash/o07.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 2e64fba..e1df37b 100644
--- a/citations.lock.json
+++ b/citations.lock.json
@@ -170,6 +170,21 @@
"first_line": "static inline int",
"lines": 6
},
+ "Include/internal/pycore_dict.h:184-185@v3.15.0rc1": {
+ "digest": "1c99b541e59b4656",
+ "first_line": "#define DKIX_EMPTY (-1)",
+ "lines": 2
+ },
+ "Include/internal/pycore_dict.h:196-235@v3.15.0rc1": {
+ "digest": "f371282052e3ed7f",
+ "first_line": "struct _dictkeysobject {",
+ "lines": 40
+ },
+ "Include/internal/pycore_dict.h:79-90@v3.15.0rc1": {
+ "digest": "c3ccf876d4deb3d8",
+ "first_line": "",
+ "lines": 12
+ },
"Include/internal/pycore_flowgraph.h:27-31@v3.15.0rc1": {
"digest": "5f318ca1d3438caa",
"first_line": "struct _PyCfgBuilder* _PyCfg_FromInstructionSequence(_PyInstructionSequence *seq);",
@@ -695,6 +710,71 @@
"first_line": "static PyObject *",
"lines": 37
},
+ "Objects/dictobject.c:1078-1101@v3.15.0rc1": {
+ "digest": "4385385afec4f891",
+ "first_line": "do_lookup(PyDictObject *mp, PyDictKeysObject *dk, PyObject *key, Py_hash_t hash,",
+ "lines": 24
+ },
+ "Objects/dictobject.c:2141-2175@v3.15.0rc1": {
+ "digest": "344810b229e48b6b",
+ "first_line": "dictresize(PyDictObject *mp,",
+ "lines": 35
+ },
+ "Objects/dictobject.c:2904-2941@v3.15.0rc1": {
+ "digest": "7a0ffae503ffbf9f",
+ "first_line": "delitem_common(PyDictObject *mp, Py_hash_t hash, Py_ssize_t ix,",
+ "lines": 38
+ },
+ "Objects/dictobject.c:329-329@v3.15.0rc1": {
+ "digest": "49bfe45f364ef798",
+ "first_line": "#define PERTURB_SHIFT 5",
+ "lines": 1
+ },
+ "Objects/dictobject.c:360-398@v3.15.0rc1": {
+ "digest": "725a960e3d558884",
+ "first_line": "The first half of collision resolution is to visit table indices via this",
+ "lines": 39
+ },
+ "Objects/dictobject.c:37-49@v3.15.0rc1": {
+ "digest": "3c2ccab161989419",
+ "first_line": "Size of indices is dk_size. Type of each index in indices varies with dk_size:",
+ "lines": 13
+ },
+ "Objects/dictobject.c:581-581@v3.15.0rc1": {
+ "digest": "142bef72e1124e8d",
+ "first_line": "#define USABLE_FRACTION(n) (((n) << 1)/3)",
+ "lines": 1
+ },
+ "Objects/dictobject.c:584-604@v3.15.0rc1": {
+ "digest": "ad8228aa7b7cea82",
+ "first_line": "static inline uint8_t",
+ "lines": 21
+ },
+ "Objects/dictobject.c:606-615@v3.15.0rc1": {
+ "digest": "e05e8d8144b5bcd0",
+ "first_line": "/* estimate_keysize is reverse function of USABLE_FRACTION.",
+ "lines": 10
+ },
+ "Objects/dictobject.c:618-628@v3.15.0rc1": {
+ "digest": "96e2b280745dc3c5",
+ "first_line": "/* GROWTH_RATE. Growth rate upon hitting maximum load.",
+ "lines": 11
+ },
+ "Objects/dictobject.c:630-640@v3.15.0rc1": {
+ "digest": "3275c5fcfd6e274a",
+ "first_line": "/* This immutable, empty PyDictKeysObject is used for PyDict_Clear()",
+ "lines": 11
+ },
+ "Objects/dictobject.c:66-89@v3.15.0rc1": {
+ "digest": "dc6f596fb1dd0c0a",
+ "first_line": "There are four kinds of slots in the table (slot is index, and",
+ "lines": 24
+ },
+ "Objects/dictobject.c:91-106@v3.15.0rc1": {
+ "digest": "1049da1243d995b5",
+ "first_line": "/*",
+ "lines": 16
+ },
"Objects/funcobject.c:1264-1270@v3.15.0rc1": {
"digest": "991dc84999f49f93",
"first_line": "func_descr_get(PyObject *func, PyObject *obj, PyObject *type)",
diff --git a/lessons/CLAIMS.md b/lessons/CLAIMS.md
index 9af969e..e98a73e 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.
-287 claims across 34 lessons, 22 of them not observable from Python.
+291 claims across 35 lessons, 22 of them not observable from Python.
## B01. Building CPython, and whether you need to
@@ -257,6 +257,15 @@ Without it the exception becomes the rule and this goes back to being a book.
| a descriptor placed in an instance dict is returned as itself, because the lookup that would call __get__ only runs for objects found on the type | [`o06-24`](o06-what-property-actually-is/o06.ipynb) |
| __set_name__ runs once for every entry in the class dict during class creation, and never runs again for later assignments to the class | [`o06-26`](o06-what-property-actually-is/o06.ipynb) |
+## O07. Two arrays and a hash
+
+| Claim | Proved by |
+| --- | --- |
+| the probe order for a size 8 table starting at slot 0 is 0, 1, 6, 7, 4, 5, 2, 3, which visits every slot exactly once before repeating | [`o07-08`](o07-two-arrays-and-a-hash/o07.ipynb) |
+| a dict grows when its entry array is full rather than when its length crosses a threshold, so the resize points fall at 6, 11, 22, 43 and 86 keys | [`o07-10`](o07-two-arrays-and-a-hash/o07.ipynb) |
+| a dict that is deleted from and inserted into in equal measure still resizes, because the entry array only ever gets compacted by a resize | [`o07-16`](o07-two-arrays-and-a-hash/o07.ipynb) |
+| a dict with only exact str keys uses a smaller entry layout, and putting one non string key in converts the whole table to the larger one | [`o07-21`](o07-two-arrays-and-a-hash/o07.ipynb) |
+
## T01. One line, seven stages
| Claim | Proved by |
diff --git a/lessons/README.md b/lessons/README.md
index 6fdbae3..bcf20e3 100644
--- a/lessons/README.md
+++ b/lessons/README.md
@@ -38,6 +38,7 @@ Each lesson is a notebook you can run. There is nothing to install and nothing t
| [O04. The order things are found in](o04-the-order-things-are-found-in/o04.ipynb) | The MRO as a list computed once and stored on the type rather than a search done every time, the C3 merge written out in twenty lines of Python and checked against __mro__ for classes from this notebook and from the standard library, the fast path that skips the merge entirely for single inheritance, the two error messages you get when no order exists and when a base is repeated, why super reads the MRO of the instance and not of the class the call was written in, and how assigning __bases__ or writing a metaclass mro changes the answer afterwards | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o04-the-order-things-are-found-in/o04.ipynb) |
| [O05. What a dot does](o05-what-a-dot-does/o05.ipynb) | PyObject_GenericGetAttr in the order it actually runs, the one if statement that is the whole of the precedence rule people memorise, the two attribute hooks that differ by five characters and by everything else, the separate function a class lookup goes through and the None it passes in place of an instance, where the did you mean suggestion is really computed, the per interpreter cache keyed on a version number that gets zeroed rather than swept, and the four different instructions the same line of Python turns into once it has run enough times | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o05-what-a-dot-does/o05.ipynb) |
| [O06. What property actually is](o06-what-property-actually-is/o06.ipynb) | Any object in a class dict with a __get__ on its type, and the four features that turn out to be exactly that, three of them under ten lines of C, the single non null pointer that decides the precedence question for every attribute in the language, func_descr_get and the two pointer object that is where self comes from, the classmethod argument that makes alternative constructors inherit, the five descriptor types an extension module gets instead of a class body and which two of them type check before touching memory, the instance dict entry that sits there being ignored, and type_new_set_names walking a copy of the class dict once and never again | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o06-what-property-actually-is/o06.ipynb) |
+| [O07. Two arrays and a hash](o07-two-arrays-and-a-hash/o07.ipynb) | dk_indices and dk_entries and which of the two is allowed to have holes in it, the -1 that ends a failed lookup in one byte read and the -2 that must not, the i * 5 + perturb + 1 recurrence and the 0, 1, 6, 7, 4, 5, 2, 3 order it gives a size 8 table, a compact dict written out in Python that resizes at 6, 11, 22, 43 and 86 keys just like the real one and iterates identically after deletes, the churn loop that grows a dict whose length never changes, the two entry layouts and the word per row a string keyed dict saves, and the presize function that is USABLE_FRACTION run backwards | [](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o07-two-arrays-and-a-hash/o07.ipynb) |
## The three programs
diff --git a/lessons/o07-two-arrays-and-a-hash/build.py b/lessons/o07-two-arrays-and-a-hash/build.py
new file mode 100644
index 0000000..abc9872
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/build.py
@@ -0,0 +1,425 @@
+#!/usr/bin/env python
+"""O07. Two arrays and a hash.
+
+The seventh lesson of the object model part. Six lessons have now said "look in the instance
+dict" or "look in the class dict" as if that were one simple step. This one opens the dict.
+
+The shape is a small array of slot numbers sitting in front of a plain append only array of
+entries. That split is where insertion order comes from, why deleting frees nothing, why a
+dict of strings is smaller than a dict of ints, and why the table resizes at 6, 11, 22, 43 and
+86 keys rather than at round numbers.
+
+The lesson builds the whole thing in Python, in about sixty lines, and checks that it grows at
+exactly the same key counts as the real one and produces exactly the same iteration order.
+
+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("o07-two-arrays-and-a-hash", "o07")
+badge = lesson.badge
+cite = lesson.cite
+term = lesson.term
+figure = Diagrams("o07-two-arrays-and-a-hash").figure
+
+
+lesson.md(f"""
+# O07. Two arrays and a hash
+
+{badge}
+
+Six lessons have said "look in the instance dict" or "walk the class dicts" and moved on. Time to open one.
+
+A dict is not one table. It is two arrays. A small one holding slot numbers, and a plain append only one holding the actual rows.
+
+{figure("two-arrays-not-one", "the index array with holes, and the entry array without any")}
+
+Only the small array has holes in it. The entry array is filled front to back and never reordered, which is where insertion order comes from and why it costs nothing.
+
+This lesson builds the whole structure in Python and checks it against the real one, key by key.
+""")
+
+
+lesson.md("""
+## About the source references
+
+Now and then this lesson points at CPython's own source, like this: `Objects/dictobject.c:1078-1101@v3.15.0rc1#do_lookup`.
+
+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, though a few of the byte counts depend on whether pointers are 8 bytes or 4.
+""")
+
+
+lesson.code(
+ """
+import pyxray
+
+pyxray.show()
+""",
+ differs=BANNER,
+ quiet=True,
+)
+
+
+lesson.md(f"""
+## Two arrays, not one
+
+The struct is `PyDictKeysObject`, {cite("Include/internal/pycore_dict.h:196-235@v3.15.0rc1#_dictkeysobject")}. Strip the free threading bits and the version counter and what is left is a size, two counters, and one flexible array member called `dk_indices`. The entry array follows it in the same allocation.
+
+`dk_indices` has one slot per table position. Each slot holds a row number into the entry array, or `-1` for a position that has never been used, or `-2` for one that used to hold something. Those two constants are `DKIX_EMPTY` and `DKIX_DUMMY`, {cite("Include/internal/pycore_dict.h:184-185@v3.15.0rc1#DKIX_DUMMY")}, and the four states a slot can be in are written out in the source, {cite("Objects/dictobject.c:66-89@v3.15.0rc1")}.
+
+The entry array is where the real data lives, in insertion order, appended to and never shuffled. Iterating a dict reads that array top to bottom and skips the cleared rows. There is no ordering machinery anywhere, {cite("Objects/dictobject.c:91-106@v3.15.0rc1")}. The order is a side effect of the layout.
+
+A slot number is small, so `dk_indices` uses the narrowest integer it can: one byte per slot while the table has 128 slots or fewer, then two, then four, {cite("Objects/dictobject.c:37-49@v3.15.0rc1")}. A dict with five keys spends eight bytes on its index array.
+""")
+
+
+lesson.md(f"""
+## Where a key lands
+
+Take the hash, keep the low bits, and that is the slot to look at first.
+
+{figure("where-a-key-lands", "the first probe, which is usually the only one")}
+
+For a table with eight slots the mask is 7, so the slot is `hash & 7`. Read `dk_indices[slot]`. A `-1` there means the key is not in the dict and the lookup is over, which is what makes a failed lookup as cheap as a successful one. A row number means something is there, and it might be your key or it might be a collision, so the entry gets compared.
+
+On a collision the search moves on, and the step it takes is the interesting part, {cite("Objects/dictobject.c:360-398@v3.15.0rc1")}.
+
+{figure("the-probe-recurrence", "the three lines that pick the next slot")}
+
+`perturb` starts as the whole hash, and each round shifts it right by 5, {cite("Objects/dictobject.c:329@v3.15.0rc1#PERTURB_SHIFT")}. The next slot is `mask & (i * 5 + perturb + 1)`. Two things are going on. The `i * 5 + 1` part on its own visits every slot in the table exactly once, in an order that is deliberately not the order consecutive keys arrive in. Adding `perturb` pulls in the high bits of the hash that the mask threw away, so two keys that start in the same slot almost immediately diverge.
+
+The loop is {cite("Objects/dictobject.c:1078-1101@v3.15.0rc1#do_lookup")}, and it is written out twice in a row inside the `for`, unrolled by hand.
+
+{lesson.claim("the probe order for a size 8 table starting at slot 0 is 0, 1, 6, 7, 4, 5, 2, 3, which visits every slot exactly once before repeating")}
+""")
+
+
+lesson.code("""
+def probe_order(hash_value, log2_size):
+ \"\"\"The same three lines as do_lookup, as a generator of slot numbers.\"\"\"
+ mask = (1 << log2_size) - 1
+ perturb = hash_value % (1 << sys.hash_info.width)
+ i = hash_value & mask
+ while True:
+ yield i
+ perturb >>= 5
+ i = mask & (i * 5 + perturb + 1)
+
+
+walk = probe_order(0, 3)
+first_nine = [next(walk) for _ in range(9)]
+print(f" starting from a hash of 0 {first_nine}")
+print(f" every slot exactly once {sorted(first_nine[:8]) == list(range(8))}")
+print(f" and then it wraps {first_nine[8] == first_nine[0]}")
+""")
+
+
+lesson.md(f"""
+That order, `0, 1, 6, 7, 4, 5, 2, 3`, is written out in the source comment too. It matters that it is not `0, 1, 2, 3`. Consecutive integers hash to consecutive values, so linear probing would make every one of them walk over its neighbours. Jumping by five is unlikely to line up with anything real.
+
+## Building the whole thing
+
+Here is the structure, in Python. Two arrays, the probe loop above for lookups, a second probe loop that stops at the first free slot for inserts, and one resize rule.
+
+The resize rule is two macros. A table is never allowed past two thirds full, {cite("Objects/dictobject.c:581@v3.15.0rc1#USABLE_FRACTION")}, and when it fills, the new size is the smallest power of two that fits three times the number of live keys, {cite("Objects/dictobject.c:618-628@v3.15.0rc1#GROWTH_RATE")} and {cite("Objects/dictobject.c:584-604@v3.15.0rc1#calculate_log2_keysize")}. Three times rather than two so that a dict which is being deleted from as much as inserted into still has room.
+
+{lesson.claim("a dict grows when its entry array is full rather than when its length crosses a threshold, so the resize points fall at 6, 11, 22, 43 and 86 keys")}
+""")
+
+
+lesson.code("""
+EMPTY = -1
+DUMMY = -2
+
+
+def usable(size):
+ \"\"\"USABLE_FRACTION: how many entries a table of this size may hold.\"\"\"
+ return (size * 2) // 3
+
+
+def new_log2_size(live_keys):
+ \"\"\"GROWTH_RATE fed into calculate_log2_keysize: room for three times the live keys.\"\"\"
+ return (max(live_keys * 3, 8) - 1).bit_length()
+
+
+class Compact:
+ \"\"\"A dict, in the same shape CPython uses. Enough of one to compare against.\"\"\"
+
+ def __init__(self):
+ self.indices = [EMPTY] * 8
+ self.entries = []
+ self.used = 0
+
+ def _lookup(self, key, key_hash):
+ for slot in probe_order(key_hash, len(self.indices).bit_length() - 1):
+ row = self.indices[slot]
+ if row == EMPTY:
+ return slot, EMPTY
+ if row >= 0:
+ stored_hash, stored_key, _ = self.entries[row]
+ if stored_hash == key_hash and (stored_key is key or stored_key == key):
+ return slot, row
+ raise AssertionError("the probe order always finds an empty slot")
+
+ def _free_slot(self, key_hash):
+ for slot in probe_order(key_hash, len(self.indices).bit_length() - 1):
+ if self.indices[slot] < 0:
+ return slot
+ raise AssertionError("the probe order always finds an empty slot")
+
+ def _resize(self, log2_size):
+ self.entries = [row for row in self.entries if row is not None]
+ self.indices = [EMPTY] * (1 << log2_size)
+ for row, (stored_hash, _, _) in enumerate(self.entries):
+ self.indices[self._free_slot(stored_hash)] = row
+
+ def __setitem__(self, key, value):
+ key_hash = hash(key)
+ slot, row = self._lookup(key, key_hash)
+ if row >= 0:
+ stored_hash, stored_key, _ = self.entries[row]
+ self.entries[row] = (stored_hash, stored_key, value)
+ return
+ if usable(len(self.indices)) - len(self.entries) <= 0:
+ self._resize(new_log2_size(self.used))
+ slot = self._free_slot(key_hash)
+ self.indices[slot] = len(self.entries)
+ self.entries.append((key_hash, key, value))
+ self.used += 1
+
+ def __delitem__(self, key):
+ slot, row = self._lookup(key, hash(key))
+ if row < 0:
+ raise KeyError(key)
+ self.indices[slot] = DUMMY
+ self.entries[row] = None
+ self.used -= 1
+
+ def __iter__(self):
+ return (row[1] for row in self.entries if row is not None)
+
+ def size(self):
+ return len(self.indices)
+""")
+
+
+lesson.md("""
+Sixty lines, and it should behave like the real thing. The test is not that it looks right, it is that it grows at the same moments and iterates in the same order.
+""")
+
+
+lesson.code("""
+mine, real = Compact(), {}
+mine_grew, real_grew = [], []
+mine_size, real_size = mine.size(), sys.getsizeof(real)
+
+for n in range(1, 200):
+ key = f"k{n}"
+ mine[key] = n
+ real[key] = n
+ if mine.size() != mine_size:
+ mine_grew.append(n)
+ mine_size = mine.size()
+ if sys.getsizeof(real) != real_size:
+ real_grew.append(n)
+ real_size = sys.getsizeof(real)
+
+print(f" my table grew at {mine_grew}")
+print(f" the real one at {real_grew}")
+print(f" same iteration order {list(mine) == list(real)}")
+""")
+
+
+lesson.md(f"""
+Same growth points and the same order over two hundred keys.
+
+The real dict has one extra entry in its list, the very first key. An empty dict does not own a keys object at all. It points at a single shared immortal one with no room in it, {cite("Objects/dictobject.c:630-640@v3.15.0rc1#empty_keys_struct")}, so the first insert has to allocate. After that the two agree exactly.
+
+Now delete a few keys and add one, which is the case where a design that moved entries around would fall apart.
+""")
+
+
+lesson.code("""
+for key in ["k3", "k17", "k50"]:
+ del mine[key]
+ del real[key]
+
+mine["late"] = 0
+real["late"] = 0
+
+print(f" same order after deletes {list(mine) == list(real)}")
+print(f" first six keys {list(mine)[:6]}")
+print(f" last key {list(mine)[-1]}")
+""")
+
+
+lesson.md(f"""
+## Deleting leaves a mark
+
+Look at what `__delitem__` did. It wrote `-2` into the index slot and cleared the entry row. It did not shrink either array, and it did not move anything.
+
+{figure("delete-leaves-a-mark", "what a delete changes and what it deliberately does not")}
+
+The dummy has to be there. A `-1` means "stop looking", so turning a deleted slot back into `-1` would cut off the probe sequence for any key that had collided with it and been pushed further along. The C does exactly the same two writes, {cite("Objects/dictobject.c:2904-2941@v3.15.0rc1#delitem_common")}.
+
+The entry row is a different matter. Its space is gone until the next resize, and the resize counter never gets it back. So a dict you delete from and insert into in a loop keeps growing even though its length never changes.
+
+{lesson.claim("a dict that is deleted from and inserted into in equal measure still resizes, because the entry array only ever gets compacted by a resize")}
+""")
+
+
+lesson.code(
+ """
+churn = {}
+for n in range(5):
+ churn[f"k{n}"] = n
+
+print(f" five keys {sys.getsizeof(churn):4} bytes")
+
+for n in range(5, 10):
+ del churn[f"k{n - 5}"]
+ churn[f"k{n}"] = n
+
+print(f" after five swaps {sys.getsizeof(churn):4} bytes, still {len(churn)} keys")
+print(f" and in order {list(churn)}")
+""",
+ varies="These are byte counts, so they halve on a 32 bit build, which is what a browser "
+ "gives you. The dict growing while its length stays at five is the part to read.",
+)
+
+
+lesson.md("""
+Five keys in, five out, five in. The length never went above five and the dict is a size class larger than it started. That is the reason `dict.clear()` exists as something other than a loop, and the reason a long lived dict used as a cache is worth rebuilding now and then.
+
+`popitem()` is the one delete that does give the space back, because it always takes the last row of the entry array and can just decrement the counter.
+""")
+
+
+lesson.code("""
+d = dict.fromkeys("abcde")
+print(f" popitem {d.popitem()}")
+print(f" popitem {d.popitem()}")
+print(f" left {list(d)}")
+""")
+
+
+lesson.md(f"""
+## Two kinds of entry
+
+There are two entry layouts, {cite("Include/internal/pycore_dict.h:79-90@v3.15.0rc1#PyDictKeyEntry")}. The general one holds a hash, a key and a value. The unicode one holds a key and a value, and no hash.
+
+{figure("two-kinds-of-entry", "the two entry layouts and when each one is used")}
+
+Dropping the hash is safe when every key is an exact `str`, because a string caches its own hash inside the string object. Storing it again in the entry would be a redundant word per row. So a dict whose keys are all strings uses two words per row instead of three.
+
+Almost every dict in a running program is that kind. Instance dicts, class dicts, module globals and keyword arguments all have string keys.
+
+{lesson.claim("a dict with only exact str keys uses a smaller entry layout, and putting one non string key in converts the whole table to the larger one")}
+""")
+
+
+lesson.code(
+ """
+strings = {"a": 1, "b": 2}
+numbers = {1: "a", 2: "b"}
+print(f" two string keys {sys.getsizeof(strings)} bytes")
+print(f" two int keys {sys.getsizeof(numbers)} bytes")
+
+strings[1] = "and now a number"
+print(f" after adding one int key to the string dict {sys.getsizeof(strings)} bytes")
+""",
+ varies="Byte counts again, so a 32 bit build halves them. The string keyed dict being the "
+ "smaller of the two, and jumping when a non string key arrives, holds everywhere.",
+)
+
+
+lesson.md(f"""
+The conversion happens inside the resize, {cite("Objects/dictobject.c:2141-2175@v3.15.0rc1#dictresize")}, which takes a flag saying whether the new table can stay unicode and clears it if the old one was already general.
+
+## Asking for the space up front
+
+One last piece. If you know how many keys are coming, there is a reverse of `USABLE_FRACTION` that works out the table size needed to hold them without any resizing, {cite("Objects/dictobject.c:606-615@v3.15.0rc1#estimate_log2_keysize")}.
+
+`dict.fromkeys` on something with a known length uses it. Building the same dict a key at a time does not, and goes through every intermediate size on the way. The end result is the same size either way, because the growth rule lands on the same power of two. What differs is the work in between.
+""")
+
+
+lesson.code(
+ """
+presized = dict.fromkeys(range(1000))
+
+built = {}
+for n in range(1000):
+ built[n] = None
+
+print(f" presized {sys.getsizeof(presized)} bytes")
+print(f" built up {sys.getsizeof(built)} bytes")
+print(f" same size {sys.getsizeof(presized) == sys.getsizeof(built)}")
+print(" but the second one allocated and rehashed its way through eight smaller tables")
+""",
+ varies="The two byte counts are smaller on a 32 bit build. That they match each other is "
+ "the point, and that holds everywhere.",
+)
+
+
+lesson.md("""
+## Try it yourself
+
+Three things to poke at.
+
+Give `Compact` a `__getitem__` that counts how many slots the probe loop visited, then fill it with a thousand keys and take the average. It should be close to one. Then swap `probe_order` for plain linear probing, `i = (i + 1) & mask`, fill it with `range(1000)` as keys, and watch the average climb.
+
+Make the dummy slots break something. Take out the `DUMMY` write in `__delitem__` and put `EMPTY` there instead. It will pass a simple test and then lose a key, and you will need a collision to see it, so start with keys that share a slot.
+
+Count the resizes. Add a counter to `_resize` and compare building a dict with a loop against `dict.fromkeys` for a few different sizes. The number is small, which is the point of doubling, but it is not zero.
+
+## What just happened
+
+A dict is a small array of slot numbers in front of an append only array of entries. The slot array has the holes. The entry array does not, which is why iteration is in insertion order and why that order costs nothing to maintain.
+
+A lookup masks the hash down to a slot number, reads one small integer, and stops right there if it is `-1`. On a collision it moves on by `i * 5 + perturb + 1`, which visits every slot exactly once and pulls in the high bits of the hash that the mask discarded. For a size 8 table the order is 0, 1, 6, 7, 4, 5, 2, 3.
+
+The table is never more than two thirds full, and when the entry array fills, it is rebuilt at the smallest power of two that fits three times the live keys. That puts the resize points at 6, 11, 22, 43 and 86 rather than anywhere round. Sixty lines of Python reproduce all of it, including the order after deletes.
+
+Deleting writes a dummy into the slot array so probe sequences stay intact, and clears the entry row without reclaiming it. Delete and insert in equal measure and the dict still grows. `popitem` is the exception, because it takes the last row.
+
+A dict with only exact string keys stores no hashes, because strings cache their own, so its rows are two words instead of three. One non string key converts the whole table at the next resize.
+
+## What is next
+
+O08 is the layout this lesson left out. Every instance of a class tends to have the same attribute names, so the keys array can be shared between all of them and each instance keeps only a bare array of values. That is a split table, it is why `__dict__` looks like a dict but is not always stored as one, and it is most of the reason ordinary Python objects are as small as they are.
+""")
+
+
+raise SystemExit(lesson.save())
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams.py b/lessons/o07-two-arrays-and-a-hash/diagrams.py
new file mode 100644
index 0000000..45c05e5
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+"""The diagrams for O07, how a dict is laid out and how it grows.
+
+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 `two-arrays-not-one`. A dict is a small array of slot numbers
+in front of a plain append only array of entries, and almost everything surprising about
+dicts comes from that split.
+"""
+
+from nbdiagram import Gallery, figures
+
+gallery = Gallery("o07-two-arrays-and-a-hash")
+
+gallery.add(
+ figures.table(
+ "two-arrays-not-one",
+ ["dk_indices", "holds", "dk_entries", "holds"],
+ [
+ ["slot 0", "-1, never used", "entry 0", "hash, 'name', 'ada'"],
+ ["slot 1", "entry 2", "entry 1", "hash, 'city', 'oslo'"],
+ ["slot 2", "-1, never used", "entry 2", "hash, 'age', 41"],
+ ["slot 3", "entry 0", "", ""],
+ ["slot 4", "-1, never used", "", ""],
+ ["slot 5", "entry 1", "", ""],
+ ],
+ title="A dict is two arrays, and only the small one has holes in it",
+ caption="Reading the entries top to bottom gives you insertion order, for free.",
+ tones=["quiet", "focus", "quiet", "focus", "quiet", "focus"],
+ )
+)
+
+
+gallery.add(
+ figures.flow(
+ "where-a-key-lands",
+ [
+ "hash(key), a whole machine word",
+ "keep the low bits: i = hash & mask",
+ "read dk_indices[i]",
+ "-1 means the key is not here, stop",
+ "otherwise compare, and on a miss probe again",
+ ],
+ title="Finding a slot, which is usually the first thing tried",
+ labels=[
+ "for a size 8 table, mask is 7",
+ "one byte read for a small dict",
+ "this is the answer for every failed lookup",
+ ],
+ tones=["input", "focus", "focus", "durable", "warning"],
+ )
+)
+
+
+gallery.add(
+ figures.stack(
+ "the-probe-recurrence",
+ [
+ "perturb starts as the whole hash",
+ "perturb >>= 5",
+ "i = mask & (i * 5 + perturb + 1)",
+ "and repeat until a slot is empty or matches",
+ ],
+ title="Three lines that decide where to look next",
+ note="For a size 8 table starting at 0 the order is 0, 1, 6, 7, 4, 5, 2, 3, which is "
+ "every slot exactly once.",
+ )
+)
+
+
+gallery.add(
+ figures.bars(
+ "when-it-grows",
+ [
+ ("6th key", 16),
+ ("11th key", 32),
+ ("22nd key", 64),
+ ("43rd key", 128),
+ ("86th key", 256),
+ ],
+ title="The table size after each resize, and the key that triggered it",
+ caption="A table is never more than two thirds full, and each resize doubles it.",
+ )
+)
+
+
+gallery.add(
+ figures.compare(
+ "delete-leaves-a-mark",
+ (
+ "the slot in dk_indices",
+ [
+ "becomes -2, a dummy",
+ "not -1, which would end a probe",
+ "a later insert can reuse it",
+ ],
+ ),
+ (
+ "the row in dk_entries",
+ [
+ "is cleared but stays put",
+ "so nothing after it moves",
+ "and its space is not given back",
+ ],
+ ),
+ title="Why deleting from a dict does not free anything",
+ verdict="Delete and insert in a loop and the entry array fills up, so the dict resizes "
+ "while its length never changes.",
+ verdict_tone="warning",
+ )
+)
+
+
+gallery.add(
+ figures.table(
+ "two-kinds-of-entry",
+ ["dk_kind", "what a row holds", "bytes per row", "when you get it"],
+ [
+ ["DICT_KEYS_UNICODE", "key, value", "16", "every key is an exact str"],
+ ["DICT_KEYS_GENERAL", "hash, key, value", "24", "anything else is a key"],
+ ],
+ title="A dict with only string keys does not store the hashes",
+ caption="A str caches its own hash, so the copy in the entry would be redundant.",
+ tones=["focus", "quiet"],
+ )
+)
+
+
+raise SystemExit(gallery.save())
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams/delete-leaves-a-mark.excalidraw b/lessons/o07-two-arrays-and-a-hash/diagrams/delete-leaves-a-mark.excalidraw
new file mode 100644
index 0000000..bccc886
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/delete-leaves-a-mark.excalidraw
@@ -0,0 +1,613 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "eb25f652c5a3420faeed",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 629.28,
+ "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 deleting from a dict does not free anything",
+ "originalText": "Why deleting from a dict does not free anything",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "d06b36a4cadb20348683",
+ "type": "text",
+ "x": 83.5,
+ "y": 50.0,
+ "width": 237.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,
+ "customData": null,
+ "text": "the slot in dk_indices",
+ "originalText": "the slot in dk_indices",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "94a11a01053cdafa92f6",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 87.0,
+ "width": 404.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": "f1bfc4d6e16f6f042f87",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "f1bfc4d6e16f6f042f87",
+ "type": "text",
+ "x": 16.0,
+ "y": 103.0,
+ "width": 372.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": "becomes -2, a dummy",
+ "originalText": "becomes -2, a dummy",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "94a11a01053cdafa92f6",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "88efcfc07b2c7cade50d",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 147.0,
+ "width": 404.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": "feb7b7330c6e12028f0a",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "feb7b7330c6e12028f0a",
+ "type": "text",
+ "x": 16.0,
+ "y": 163.0,
+ "width": 372.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": "not -1, which would end a probe",
+ "originalText": "not -1, which would end a probe",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "88efcfc07b2c7cade50d",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "9956cbb4fc28a39529f0",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 207.0,
+ "width": 404.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": "98d52fb924238de1b0de",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "98d52fb924238de1b0de",
+ "type": "text",
+ "x": 16.0,
+ "y": 223.0,
+ "width": 372.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": "a later insert can reuse it",
+ "originalText": "a later insert can reuse it",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "9956cbb4fc28a39529f0",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "821954fe10904fb4c701",
+ "type": "text",
+ "x": 549.9,
+ "y": 50.0,
+ "width": 232.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": "the row in dk_entries",
+ "originalText": "the row in dk_entries",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "ec9982ae171e3206b0ee",
+ "type": "rectangle",
+ "x": 464.0,
+ "y": 87.0,
+ "width": 404.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": "ce23a80b204395ca9778",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "ce23a80b204395ca9778",
+ "type": "text",
+ "x": 480.0,
+ "y": 103.0,
+ "width": 372.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": "is cleared but stays put",
+ "originalText": "is cleared but stays put",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "ec9982ae171e3206b0ee",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "f448c57d3ec6ba106f3e",
+ "type": "rectangle",
+ "x": 464.0,
+ "y": 147.0,
+ "width": 404.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": "e0601d2a80d485c0d515",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "e0601d2a80d485c0d515",
+ "type": "text",
+ "x": 480.0,
+ "y": 163.0,
+ "width": 372.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": "so nothing after it moves",
+ "originalText": "so nothing after it moves",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "f448c57d3ec6ba106f3e",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "5f3e7e9cfa03cfcba940",
+ "type": "rectangle",
+ "x": 464.0,
+ "y": 207.0,
+ "width": 404.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": "34715476100b90d04698",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "34715476100b90d04698",
+ "type": "text",
+ "x": 480.0,
+ "y": 223.0,
+ "width": 372.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": "and its space is not given back",
+ "originalText": "and its space is not given back",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "5f3e7e9cfa03cfcba940",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "679115fc0b7b041e566e",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 284.0,
+ "width": 868.0,
+ "height": 82.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": "9f0251aed4373ef79b1c",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "9f0251aed4373ef79b1c",
+ "type": "text",
+ "x": 16.0,
+ "y": 300.0,
+ "width": 836.0,
+ "height": 50.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": "Delete and insert in a loop and the entry array fills up, so the dict resizes\nwhile its length never changes.",
+ "originalText": "Delete and insert in a loop and the entry array fills up, so the dict resizes\nwhile its length never changes.",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "679115fc0b7b041e566e",
+ "lineHeight": 1.25,
+ "autoResize": false
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams/delete-leaves-a-mark.svg b/lessons/o07-two-arrays-and-a-hash/diagrams/delete-leaves-a-mark.svg
new file mode 100644
index 0000000..a61c47d
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/delete-leaves-a-mark.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams/the-probe-recurrence.excalidraw b/lessons/o07-two-arrays-and-a-hash/diagrams/the-probe-recurrence.excalidraw
new file mode 100644
index 0000000..9793414
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/the-probe-recurrence.excalidraw
@@ -0,0 +1,403 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "9e03cfaf885dcab1206a",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 559.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 lines that decide where to look next",
+ "originalText": "Three lines that decide where to look next",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "2e099ce5ae187a234ec4",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 50.0,
+ "width": 548.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": "c9ce7099118b92150206",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "c9ce7099118b92150206",
+ "type": "text",
+ "x": 16.0,
+ "y": 66.0,
+ "width": 516.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": "and repeat until a slot is empty or matches",
+ "originalText": "and repeat until a slot is empty or matches",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "2e099ce5ae187a234ec4",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "13cbef689155e61ad50f",
+ "type": "text",
+ "x": 564.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": "80423d25f331a1dd8c3e",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 102.0,
+ "width": 548.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": "6af88b1a8a1b806586cc",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "6af88b1a8a1b806586cc",
+ "type": "text",
+ "x": 16.0,
+ "y": 118.0,
+ "width": 516.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": "i = mask & (i * 5 + perturb + 1)",
+ "originalText": "i = mask & (i * 5 + perturb + 1)",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "80423d25f331a1dd8c3e",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "0c64e9a0a7363d8445a6",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 154.0,
+ "width": 548.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": "44a7c976c4348e4acf6d",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "44a7c976c4348e4acf6d",
+ "type": "text",
+ "x": 16.0,
+ "y": 170.0,
+ "width": 516.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": "perturb >>= 5",
+ "originalText": "perturb >>= 5",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "0c64e9a0a7363d8445a6",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "cd157fdb042a6af5ea08",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 206.0,
+ "width": 548.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": "cd6b43fe45596259aaa4",
+ "type": "text"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "cd6b43fe45596259aaa4",
+ "type": "text",
+ "x": 16.0,
+ "y": 222.0,
+ "width": 516.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": "perturb starts as the whole hash",
+ "originalText": "perturb starts as the whole hash",
+ "fontSize": 20,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "middle",
+ "containerId": "cd157fdb042a6af5ea08",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "d616dfe1c388212581d2",
+ "type": "text",
+ "x": 0.0,
+ "y": 278.0,
+ "width": 890.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": "For a size 8 table starting at 0 the order is 0, 1, 6, 7, 4, 5, 2, 3, which is every slot exactly once.",
+ "originalText": "For a size 8 table starting at 0 the order is 0, 1, 6, 7, 4, 5, 2, 3, which is every slot exactly once.",
+ "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/o07-two-arrays-and-a-hash/diagrams/the-probe-recurrence.svg b/lessons/o07-two-arrays-and-a-hash/diagrams/the-probe-recurrence.svg
new file mode 100644
index 0000000..266f0ee
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/the-probe-recurrence.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams/two-arrays-not-one.excalidraw b/lessons/o07-two-arrays-and-a-hash/diagrams/two-arrays-not-one.excalidraw
new file mode 100644
index 0000000..ea4151f
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/two-arrays-not-one.excalidraw
@@ -0,0 +1,1694 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "5b0fc60325c4c2833020",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 800.28,
+ "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": "A dict is two arrays, and only the small one has holes in it",
+ "originalText": "A dict is two arrays, and only the small one has holes in it",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "637669b7d9a957a3cd8e",
+ "type": "text",
+ "x": 14.0,
+ "y": 50.0,
+ "width": 88.8,
+ "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": "dk_indices",
+ "originalText": "dk_indices",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "6e9bcff94ddb99639ba2",
+ "type": "text",
+ "x": 138.0,
+ "y": 50.0,
+ "width": 44.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": "holds",
+ "originalText": "holds",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "17ee1f7480a1fd4d7dd5",
+ "type": "text",
+ "x": 300.4,
+ "y": 50.0,
+ "width": 85.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": "dk_entries",
+ "originalText": "dk_entries",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "bb84dbf8797ba24151a3",
+ "type": "text",
+ "x": 424.4,
+ "y": 50.0,
+ "width": 44.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": "holds",
+ "originalText": "holds",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "8cd0625db1d04e30f000",
+ "type": "line",
+ "x": 0.0,
+ "y": 76.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "d36ce3af286c9dfd0b3e",
+ "type": "line",
+ "x": 0.0,
+ "y": 76.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ],
+ [
+ 630.4,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "3290b2d46993c772898b",
+ "type": "text",
+ "x": 14.0,
+ "y": 86.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": "slot 0",
+ "originalText": "slot 0",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "af40cbf3f1101bfe8cc7",
+ "type": "text",
+ "x": 138.0,
+ "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": "-1, never used",
+ "originalText": "-1, never used",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "e495add86f0b8f5c2dd8",
+ "type": "text",
+ "x": 300.4,
+ "y": 86.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": "entry 0",
+ "originalText": "entry 0",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "b0187bb22c27287747f5",
+ "type": "text",
+ "x": 424.4,
+ "y": 86.0,
+ "width": 182.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": "hash, 'name', 'ada'",
+ "originalText": "hash, 'name', 'ada'",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "daa2337e5e44f867c838",
+ "type": "line",
+ "x": 0.0,
+ "y": 116.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "9d34d6db316d845ce031",
+ "type": "line",
+ "x": 0.0,
+ "y": 116.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ],
+ [
+ 630.4,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "36fbf0f43128cd7e55b8",
+ "type": "text",
+ "x": 14.0,
+ "y": 126.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": "slot 1",
+ "originalText": "slot 1",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "a7779c6a76eb44886eaf",
+ "type": "text",
+ "x": 138.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": "entry 2",
+ "originalText": "entry 2",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "144bc7b20f3356943de2",
+ "type": "text",
+ "x": 300.4,
+ "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": "entry 1",
+ "originalText": "entry 1",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "874db9faba16519aeb8e",
+ "type": "text",
+ "x": 424.4,
+ "y": 126.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": "hash, 'city', 'oslo'",
+ "originalText": "hash, 'city', 'oslo'",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "7e47ffb491ea61cf34f9",
+ "type": "line",
+ "x": 0.0,
+ "y": 156.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "da9af94431509df51026",
+ "type": "line",
+ "x": 0.0,
+ "y": 156.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ],
+ [
+ 630.4,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "e50f0bfad8faf000a9da",
+ "type": "text",
+ "x": 14.0,
+ "y": 166.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": "slot 2",
+ "originalText": "slot 2",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "5ca55f016d941cedf939",
+ "type": "text",
+ "x": 138.0,
+ "y": 166.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": "-1, never used",
+ "originalText": "-1, never used",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "1fb2f50aaaa348ebeeca",
+ "type": "text",
+ "x": 300.4,
+ "y": 166.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": "entry 2",
+ "originalText": "entry 2",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "6e9576bd7c7a8e246a55",
+ "type": "text",
+ "x": 424.4,
+ "y": 166.0,
+ "width": 144.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": "hash, 'age', 41",
+ "originalText": "hash, 'age', 41",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "f2a5586cb05433889502",
+ "type": "line",
+ "x": 0.0,
+ "y": 196.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "839e2d2a43f42eab9715",
+ "type": "line",
+ "x": 0.0,
+ "y": 196.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ],
+ [
+ 630.4,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "3f017e4412b238c014db",
+ "type": "text",
+ "x": 14.0,
+ "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": "slot 3",
+ "originalText": "slot 3",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "23bb43b2e0ea69bc8f0f",
+ "type": "text",
+ "x": 138.0,
+ "y": 206.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": "entry 0",
+ "originalText": "entry 0",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "25d83e9d776139188f79",
+ "type": "text",
+ "x": 300.4,
+ "y": 206.0,
+ "width": 0.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": "",
+ "originalText": "",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "9d4bb1119ea62d9fbbaa",
+ "type": "text",
+ "x": 424.4,
+ "y": 206.0,
+ "width": 0.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": "",
+ "originalText": "",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "e39688ac8db2c17f0dcd",
+ "type": "line",
+ "x": 0.0,
+ "y": 236.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "047baf92d9a7010d8d2b",
+ "type": "line",
+ "x": 0.0,
+ "y": 236.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ],
+ [
+ 630.4,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "32f05bbfc6908419e08d",
+ "type": "text",
+ "x": 14.0,
+ "y": 246.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": "slot 4",
+ "originalText": "slot 4",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "6fdfb4da6d0f4795d2b3",
+ "type": "text",
+ "x": 138.0,
+ "y": 246.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": "-1, never used",
+ "originalText": "-1, never used",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "bb9781518dad13458b71",
+ "type": "text",
+ "x": 300.4,
+ "y": 246.0,
+ "width": 0.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": "",
+ "originalText": "",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "245a75bfb5ac50c4e00a",
+ "type": "text",
+ "x": 424.4,
+ "y": 246.0,
+ "width": 0.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": "",
+ "originalText": "",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "54efcdd98886a44a79ad",
+ "type": "line",
+ "x": 0.0,
+ "y": 276.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "c5abfd129fbb11dc9438",
+ "type": "line",
+ "x": 0.0,
+ "y": 276.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ],
+ [
+ 630.4,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "6746e4a3caef8a754569",
+ "type": "text",
+ "x": 14.0,
+ "y": 286.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": "slot 5",
+ "originalText": "slot 5",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "8a890747e27ca93c1024",
+ "type": "text",
+ "x": 138.0,
+ "y": 286.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": "entry 1",
+ "originalText": "entry 1",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "74676755981087937549",
+ "type": "text",
+ "x": 300.4,
+ "y": 286.0,
+ "width": 0.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": "",
+ "originalText": "",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "541e0aacbde64ffd2ca7",
+ "type": "text",
+ "x": 424.4,
+ "y": 286.0,
+ "width": 0.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": "",
+ "originalText": "",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "02e868479959a5b36ecb",
+ "type": "line",
+ "x": 0.0,
+ "y": 316.0,
+ "width": 630.4,
+ "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
+ ],
+ [
+ 630.4,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "d88b8dabe9c1b09cbc1a",
+ "type": "text",
+ "x": 0.0,
+ "y": 336.0,
+ "width": 598.5600000000001,
+ "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": "Reading the entries top to bottom gives you insertion order, for free.",
+ "originalText": "Reading the entries top to bottom gives you insertion order, for free.",
+ "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/o07-two-arrays-and-a-hash/diagrams/two-arrays-not-one.svg b/lessons/o07-two-arrays-and-a-hash/diagrams/two-arrays-not-one.svg
new file mode 100644
index 0000000..3a8b37d
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/two-arrays-not-one.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams/two-kinds-of-entry.excalidraw b/lessons/o07-two-arrays-and-a-hash/diagrams/two-kinds-of-entry.excalidraw
new file mode 100644
index 0000000..645b526
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/two-kinds-of-entry.excalidraw
@@ -0,0 +1,746 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "529b7552544eed8c2e73",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 721.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": "A dict with only string keys does not store the hashes",
+ "originalText": "A dict with only string keys does not store the hashes",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "2bc1f512aefc977e48f9",
+ "type": "text",
+ "x": 14.0,
+ "y": 50.0,
+ "width": 63.599999999999994,
+ "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": "dk_kind",
+ "originalText": "dk_kind",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "42816c55d14ce356cd9b",
+ "type": "text",
+ "x": 205.2,
+ "y": 50.0,
+ "width": 154.32,
+ "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 a row holds",
+ "originalText": "what a row holds",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "59165344099e42d03eb1",
+ "type": "text",
+ "x": 386.79999999999995,
+ "y": 50.0,
+ "width": 119.75999999999999,
+ "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": "bytes per row",
+ "originalText": "bytes per row",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "2d94131b15e3e9d7dd93",
+ "type": "text",
+ "x": 539.6,
+ "y": 50.0,
+ "width": 138.95999999999998,
+ "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": "when you get it",
+ "originalText": "when you get it",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "8c720e4a2a66ef47569a",
+ "type": "line",
+ "x": 0.0,
+ "y": 76.0,
+ "width": 793.6,
+ "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
+ ],
+ [
+ 793.6,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "20cd36e4cd1b9d856bd3",
+ "type": "line",
+ "x": 0.0,
+ "y": 76.0,
+ "width": 793.6,
+ "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
+ ],
+ [
+ 793.6,
+ 0.0
+ ],
+ [
+ 793.6,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "c673585a597f3bd348ee",
+ "type": "text",
+ "x": 14.0,
+ "y": 86.0,
+ "width": 163.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": "DICT_KEYS_UNICODE",
+ "originalText": "DICT_KEYS_UNICODE",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "a9f2d811ffd93609cbb1",
+ "type": "text",
+ "x": 205.2,
+ "y": 86.0,
+ "width": 96.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": "key, value",
+ "originalText": "key, value",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "3cbacc51a0a9dfe15c18",
+ "type": "text",
+ "x": 386.79999999999995,
+ "y": 86.0,
+ "width": 19.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": "16",
+ "originalText": "16",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "e7782d7676b0da3baf0b",
+ "type": "text",
+ "x": 539.6,
+ "y": 86.0,
+ "width": 240.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": "every key is an exact str",
+ "originalText": "every key is an exact str",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "d12706c39aac67b37125",
+ "type": "line",
+ "x": 0.0,
+ "y": 116.0,
+ "width": 793.6,
+ "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
+ ],
+ [
+ 793.6,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "6fc80104dd8bd17749bb",
+ "type": "line",
+ "x": 0.0,
+ "y": 116.0,
+ "width": 793.6,
+ "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
+ ],
+ [
+ 793.6,
+ 0.0
+ ],
+ [
+ 793.6,
+ 40.0
+ ],
+ [
+ 0,
+ 40.0
+ ],
+ [
+ 0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "d37495d333027d4fa8e8",
+ "type": "text",
+ "x": 14.0,
+ "y": 126.0,
+ "width": 163.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": "DICT_KEYS_GENERAL",
+ "originalText": "DICT_KEYS_GENERAL",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "175c13f5bb45bf673bd7",
+ "type": "text",
+ "x": 205.2,
+ "y": 126.0,
+ "width": 153.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": "hash, key, value",
+ "originalText": "hash, key, value",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "c2dcf51b8957cde86b59",
+ "type": "text",
+ "x": 386.79999999999995,
+ "y": 126.0,
+ "width": 19.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": "24",
+ "originalText": "24",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "0080ba99d41bfbf0ba07",
+ "type": "text",
+ "x": 539.6,
+ "y": 126.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": "anything else is a key",
+ "originalText": "anything else is a key",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "481ff959c303804406d9",
+ "type": "line",
+ "x": 0.0,
+ "y": 156.0,
+ "width": 793.6,
+ "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
+ ],
+ [
+ 793.6,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "73d55a81fce0c48b0885",
+ "type": "text",
+ "x": 0.0,
+ "y": 176.0,
+ "width": 642.72,
+ "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": "A str caches its own hash, so the copy in the entry would be redundant.",
+ "originalText": "A str caches its own hash, so the copy in the entry would be redundant.",
+ "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/o07-two-arrays-and-a-hash/diagrams/two-kinds-of-entry.svg b/lessons/o07-two-arrays-and-a-hash/diagrams/two-kinds-of-entry.svg
new file mode 100644
index 0000000..379af67
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/two-kinds-of-entry.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams/when-it-grows.excalidraw b/lessons/o07-two-arrays-and-a-hash/diagrams/when-it-grows.excalidraw
new file mode 100644
index 0000000..d011b09
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/when-it-grows.excalidraw
@@ -0,0 +1,711 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "8ec5dd043380e4351e10",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 810.0,
+ "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 table size after each resize, and the key that triggered it",
+ "originalText": "The table size after each resize, and the key that triggered it",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "97d28f70517e4cc88aee",
+ "type": "text",
+ "x": 0.0,
+ "y": 57.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": "6th key",
+ "originalText": "6th key",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "d21fad95510fec39624a",
+ "type": "line",
+ "x": 96.8,
+ "y": 50.0,
+ "width": 38.750000000000014,
+ "height": 34.0,
+ "angle": 0,
+ "strokeColor": "#1971c2",
+ "backgroundColor": "#a5d8ff",
+ "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
+ ],
+ [
+ 38.750000000000014,
+ 0.0
+ ],
+ [
+ 38.750000000000014,
+ 34.0
+ ],
+ [
+ 0.0,
+ 34.0
+ ],
+ [
+ 0.0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "fde6cdcc09c856657376",
+ "type": "text",
+ "x": 145.55,
+ "y": 57.0,
+ "width": 19.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": "16",
+ "originalText": "16",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "c10d8087cd18ac5eabeb",
+ "type": "text",
+ "x": 0.0,
+ "y": 101.0,
+ "width": 76.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": "11th key",
+ "originalText": "11th key",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "978cf8a082c9a7129dcc",
+ "type": "line",
+ "x": 96.8,
+ "y": 94.0,
+ "width": 77.50000000000001,
+ "height": 34.0,
+ "angle": 0,
+ "strokeColor": "#6741d9",
+ "backgroundColor": "#d0bfff",
+ "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
+ ],
+ [
+ 77.50000000000001,
+ 0.0
+ ],
+ [
+ 77.50000000000001,
+ 34.0
+ ],
+ [
+ 0.0,
+ 34.0
+ ],
+ [
+ 0.0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "b8f50be823bd0275bb71",
+ "type": "text",
+ "x": 184.3,
+ "y": 101.0,
+ "width": 19.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": "32",
+ "originalText": "32",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "9d1b360805498e803951",
+ "type": "text",
+ "x": 0.0,
+ "y": 145.0,
+ "width": 76.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": "22nd key",
+ "originalText": "22nd key",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "a68da38c31b73f855481",
+ "type": "line",
+ "x": 96.8,
+ "y": 138.0,
+ "width": 155.0,
+ "height": 34.0,
+ "angle": 0,
+ "strokeColor": "#099268",
+ "backgroundColor": "#96f2d7",
+ "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
+ ],
+ [
+ 155.0,
+ 0.0
+ ],
+ [
+ 155.0,
+ 34.0
+ ],
+ [
+ 0.0,
+ 34.0
+ ],
+ [
+ 0.0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "bdf1264552ed2e52e7a0",
+ "type": "text",
+ "x": 261.8,
+ "y": 145.0,
+ "width": 19.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": "64",
+ "originalText": "64",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "3a8e1ef1f9bf3694e778",
+ "type": "text",
+ "x": 0.0,
+ "y": 189.0,
+ "width": 76.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": "43rd key",
+ "originalText": "43rd key",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "de50a24e592926e88a96",
+ "type": "line",
+ "x": 96.8,
+ "y": 182.0,
+ "width": 310.0,
+ "height": 34.0,
+ "angle": 0,
+ "strokeColor": "#e8590c",
+ "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.0
+ ],
+ [
+ 310.0,
+ 0.0
+ ],
+ [
+ 310.0,
+ 34.0
+ ],
+ [
+ 0.0,
+ 34.0
+ ],
+ [
+ 0.0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "4d76eb5d5f77bc1dccdd",
+ "type": "text",
+ "x": 416.8,
+ "y": 189.0,
+ "width": 28.799999999999997,
+ "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": "128",
+ "originalText": "128",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "a988d1f63a2e3522bd58",
+ "type": "text",
+ "x": 0.0,
+ "y": 233.0,
+ "width": 76.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": "86th key",
+ "originalText": "86th key",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "dd51c6ea4af574be8cef",
+ "type": "line",
+ "x": 96.8,
+ "y": 226.0,
+ "width": 620.0,
+ "height": 34.0,
+ "angle": 0,
+ "strokeColor": "#e03131",
+ "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.0
+ ],
+ [
+ 620.0,
+ 0.0
+ ],
+ [
+ 620.0,
+ 34.0
+ ],
+ [
+ 0.0,
+ 34.0
+ ],
+ [
+ 0.0,
+ 0.0
+ ]
+ ],
+ "lastCommittedPoint": null,
+ "polygon": true
+ },
+ {
+ "id": "6d00b4c25ef869beb958",
+ "type": "text",
+ "x": 726.8,
+ "y": 233.0,
+ "width": 28.799999999999997,
+ "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": "256",
+ "originalText": "256",
+ "fontSize": 16,
+ "fontFamily": 3,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "3af9379b761216b40139",
+ "type": "text",
+ "x": 0.0,
+ "y": 280.0,
+ "width": 621.12,
+ "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": "A table is never more than two thirds full, and each resize doubles it.",
+ "originalText": "A table is never more than two thirds full, and each resize doubles it.",
+ "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/o07-two-arrays-and-a-hash/diagrams/when-it-grows.svg b/lessons/o07-two-arrays-and-a-hash/diagrams/when-it-grows.svg
new file mode 100644
index 0000000..bf80df0
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/when-it-grows.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams/where-a-key-lands.excalidraw b/lessons/o07-two-arrays-and-a-hash/diagrams/where-a-key-lands.excalidraw
new file mode 100644
index 0000000..175f518
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/where-a-key-lands.excalidraw
@@ -0,0 +1,746 @@
+{
+ "type": "excalidraw",
+ "version": 2,
+ "source": "https://github.com/tamnd/cpython-internals",
+ "elements": [
+ {
+ "id": "8df366eb8ae16f92af48",
+ "type": "text",
+ "x": 0.0,
+ "y": 0.0,
+ "width": 683.64,
+ "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": "Finding a slot, which is usually the first thing tried",
+ "originalText": "Finding a slot, which is usually the first thing tried",
+ "fontSize": 24,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "91d2feced870f4abc2f2",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 50.0,
+ "width": 545.6,
+ "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": "cc783f257edfb87d94d1",
+ "type": "text"
+ },
+ {
+ "id": "4e57501e8e06171695c5",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "cc783f257edfb87d94d1",
+ "type": "text",
+ "x": 16.0,
+ "y": 72.5,
+ "width": 513.6,
+ "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": "hash(key), a whole machine word",
+ "originalText": "hash(key), a whole machine word",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "91d2feced870f4abc2f2",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "d56a374cf4a3067ea59b",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 180.0,
+ "width": 545.6,
+ "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": "e450da012553660472ab",
+ "type": "text"
+ },
+ {
+ "id": "4e57501e8e06171695c5",
+ "type": "arrow"
+ },
+ {
+ "id": "e273f11acb5dc4fb424e",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "e450da012553660472ab",
+ "type": "text",
+ "x": 16.0,
+ "y": 202.5,
+ "width": 513.6,
+ "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": "keep the low bits: i = hash & mask",
+ "originalText": "keep the low bits: i = hash & mask",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "d56a374cf4a3067ea59b",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "ad01b752a36cc0f5785b",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 310.0,
+ "width": 545.6,
+ "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": "50d415c90dff9c5cbae0",
+ "type": "text"
+ },
+ {
+ "id": "e273f11acb5dc4fb424e",
+ "type": "arrow"
+ },
+ {
+ "id": "b9b5d649f630a3a82245",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "50d415c90dff9c5cbae0",
+ "type": "text",
+ "x": 16.0,
+ "y": 332.5,
+ "width": 513.6,
+ "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": "read dk_indices[i]",
+ "originalText": "read dk_indices[i]",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "ad01b752a36cc0f5785b",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "dfe547cdc097be39a054",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 440.0,
+ "width": 545.6,
+ "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": "2ce13a84c412cfbd35b6",
+ "type": "text"
+ },
+ {
+ "id": "b9b5d649f630a3a82245",
+ "type": "arrow"
+ },
+ {
+ "id": "e5cacabf3ff0f4bffc85",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "2ce13a84c412cfbd35b6",
+ "type": "text",
+ "x": 16.0,
+ "y": 462.5,
+ "width": 513.6,
+ "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": "-1 means the key is not here, stop",
+ "originalText": "-1 means the key is not here, stop",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "dfe547cdc097be39a054",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "6f0a656a2e799ec02e4e",
+ "type": "rectangle",
+ "x": 0.0,
+ "y": 570.0,
+ "width": 545.6,
+ "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": "8109207d83406ba19131",
+ "type": "text"
+ },
+ {
+ "id": "e5cacabf3ff0f4bffc85",
+ "type": "arrow"
+ }
+ ],
+ "updated": 1,
+ "link": null,
+ "locked": false
+ },
+ {
+ "id": "8109207d83406ba19131",
+ "type": "text",
+ "x": 16.0,
+ "y": 592.5,
+ "width": 513.6,
+ "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": "otherwise compare, and on a miss probe again",
+ "originalText": "otherwise compare, and on a miss probe again",
+ "fontSize": 20,
+ "fontFamily": 2,
+ "textAlign": "center",
+ "verticalAlign": "middle",
+ "containerId": "6f0a656a2e799ec02e4e",
+ "lineHeight": 1.25,
+ "autoResize": false
+ },
+ {
+ "id": "4e57501e8e06171695c5",
+ "type": "arrow",
+ "x": 272.8,
+ "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": "91d2feced870f4abc2f2",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "d56a374cf4a3067ea59b",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "8a41c7d3bcbca17adfef",
+ "type": "text",
+ "x": 284.8,
+ "y": 140.0,
+ "width": 258.96,
+ "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": "for a size 8 table, mask is 7",
+ "originalText": "for a size 8 table, mask is 7",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "e273f11acb5dc4fb424e",
+ "type": "arrow",
+ "x": 272.8,
+ "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": "d56a374cf4a3067ea59b",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "ad01b752a36cc0f5785b",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "35cb4e5d30fdd8dc0958",
+ "type": "text",
+ "x": 284.8,
+ "y": 270.0,
+ "width": 264.96,
+ "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 byte read for a small dict",
+ "originalText": "one byte read for a small dict",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "b9b5d649f630a3a82245",
+ "type": "arrow",
+ "x": 272.8,
+ "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": "ad01b752a36cc0f5785b",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "dfe547cdc097be39a054",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ },
+ {
+ "id": "a942071df8b40971c78c",
+ "type": "text",
+ "x": 284.8,
+ "y": 400.0,
+ "width": 365.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": "this is the answer for every failed lookup",
+ "originalText": "this is the answer for every failed lookup",
+ "fontSize": 16,
+ "fontFamily": 2,
+ "textAlign": "left",
+ "verticalAlign": "top",
+ "containerId": null,
+ "lineHeight": 1.25,
+ "autoResize": true
+ },
+ {
+ "id": "e5cacabf3ff0f4bffc85",
+ "type": "arrow",
+ "x": 272.8,
+ "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": "dfe547cdc097be39a054",
+ "focus": 0,
+ "gap": 4
+ },
+ "endBinding": {
+ "elementId": "6f0a656a2e799ec02e4e",
+ "focus": 0,
+ "gap": 4
+ },
+ "startArrowhead": null,
+ "endArrowhead": "arrow",
+ "elbowed": false
+ }
+ ],
+ "appState": {
+ "gridSize": 20,
+ "viewBackgroundColor": "#ffffff"
+ },
+ "files": {}
+}
diff --git a/lessons/o07-two-arrays-and-a-hash/diagrams/where-a-key-lands.svg b/lessons/o07-two-arrays-and-a-hash/diagrams/where-a-key-lands.svg
new file mode 100644
index 0000000..4fc231b
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/diagrams/where-a-key-lands.svg
@@ -0,0 +1 @@
+
diff --git a/lessons/o07-two-arrays-and-a-hash/o07.ipynb b/lessons/o07-two-arrays-and-a-hash/o07.ipynb
new file mode 100644
index 0000000..85692a7
--- /dev/null
+++ b/lessons/o07-two-arrays-and-a-hash/o07.ipynb
@@ -0,0 +1,528 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "o07-01",
+ "metadata": {},
+ "source": [
+ "# O07. Two arrays and a hash\n",
+ "\n",
+ "[](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/o07-two-arrays-and-a-hash/o07.ipynb)\n",
+ "\n",
+ "Six lessons have said \"look in the instance dict\" or \"walk the class dicts\" and moved on. Time to open one.\n",
+ "\n",
+ "A dict is not one table. It is two arrays. A small one holding slot numbers, and a plain append only one holding the actual rows.\n",
+ "\n",
+ "\n",
+ "\n",
+ "Only the small array has holes in it. The entry array is filled front to back and never reordered, which is where insertion order comes from and why it costs nothing.\n",
+ "\n",
+ "This lesson builds the whole structure in Python and checks it against the real one, key by key."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-02",
+ "metadata": {},
+ "source": [
+ "## About the source references\n",
+ "\n",
+ "Now and then this lesson points at CPython's own source, like this: `Objects/dictobject.c:1078-1101@v3.15.0rc1#do_lookup`.\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": "o07-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": "o07-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, though a few of the byte counts depend on whether pointers are 8 bytes or 4."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-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": "o07-06",
+ "metadata": {},
+ "source": [
+ "## Two arrays, not one\n",
+ "\n",
+ "The struct is `PyDictKeysObject`, [Include/internal/pycore_dict.h:196-235@v3.15.0rc1#_dictkeysobject](https://github.com/python/cpython/blob/v3.15.0rc1/Include/internal/pycore_dict.h#L196-L235). Strip the free threading bits and the version counter and what is left is a size, two counters, and one flexible array member called `dk_indices`. The entry array follows it in the same allocation.\n",
+ "\n",
+ "`dk_indices` has one slot per table position. Each slot holds a row number into the entry array, or `-1` for a position that has never been used, or `-2` for one that used to hold something. Those two constants are `DKIX_EMPTY` and `DKIX_DUMMY`, [Include/internal/pycore_dict.h:184-185@v3.15.0rc1#DKIX_DUMMY](https://github.com/python/cpython/blob/v3.15.0rc1/Include/internal/pycore_dict.h#L184-L185), and the four states a slot can be in are written out in the source, [Objects/dictobject.c:66-89@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L66-L89).\n",
+ "\n",
+ "The entry array is where the real data lives, in insertion order, appended to and never shuffled. Iterating a dict reads that array top to bottom and skips the cleared rows. There is no ordering machinery anywhere, [Objects/dictobject.c:91-106@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L91-L106). The order is a side effect of the layout.\n",
+ "\n",
+ "A slot number is small, so `dk_indices` uses the narrowest integer it can: one byte per slot while the table has 128 slots or fewer, then two, then four, [Objects/dictobject.c:37-49@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L37-L49). A dict with five keys spends eight bytes on its index array."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-07",
+ "metadata": {},
+ "source": [
+ "## Where a key lands\n",
+ "\n",
+ "Take the hash, keep the low bits, and that is the slot to look at first.\n",
+ "\n",
+ "\n",
+ "\n",
+ "For a table with eight slots the mask is 7, so the slot is `hash & 7`. Read `dk_indices[slot]`. A `-1` there means the key is not in the dict and the lookup is over, which is what makes a failed lookup as cheap as a successful one. A row number means something is there, and it might be your key or it might be a collision, so the entry gets compared.\n",
+ "\n",
+ "On a collision the search moves on, and the step it takes is the interesting part, [Objects/dictobject.c:360-398@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L360-L398).\n",
+ "\n",
+ "\n",
+ "\n",
+ "`perturb` starts as the whole hash, and each round shifts it right by 5, [Objects/dictobject.c:329@v3.15.0rc1#PERTURB_SHIFT](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L329). The next slot is `mask & (i * 5 + perturb + 1)`. Two things are going on. The `i * 5 + 1` part on its own visits every slot in the table exactly once, in an order that is deliberately not the order consecutive keys arrive in. Adding `perturb` pulls in the high bits of the hash that the mask threw away, so two keys that start in the same slot almost immediately diverge.\n",
+ "\n",
+ "The loop is [Objects/dictobject.c:1078-1101@v3.15.0rc1#do_lookup](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L1078-L1101), and it is written out twice in a row inside the `for`, unrolled by hand.\n",
+ "\n",
+ "the probe order for a size 8 table starting at slot 0 is 0, 1, 6, 7, 4, 5, 2, 3, which visits every slot exactly once before repeating"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-08",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def probe_order(hash_value, log2_size):\n",
+ " \"\"\"The same three lines as do_lookup, as a generator of slot numbers.\"\"\"\n",
+ " mask = (1 << log2_size) - 1\n",
+ " perturb = hash_value % (1 << sys.hash_info.width)\n",
+ " i = hash_value & mask\n",
+ " while True:\n",
+ " yield i\n",
+ " perturb >>= 5\n",
+ " i = mask & (i * 5 + perturb + 1)\n",
+ "\n",
+ "\n",
+ "walk = probe_order(0, 3)\n",
+ "first_nine = [next(walk) for _ in range(9)]\n",
+ "print(f\" starting from a hash of 0 {first_nine}\")\n",
+ "print(f\" every slot exactly once {sorted(first_nine[:8]) == list(range(8))}\")\n",
+ "print(f\" and then it wraps {first_nine[8] == first_nine[0]}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-09",
+ "metadata": {},
+ "source": [
+ "That order, `0, 1, 6, 7, 4, 5, 2, 3`, is written out in the source comment too. It matters that it is not `0, 1, 2, 3`. Consecutive integers hash to consecutive values, so linear probing would make every one of them walk over its neighbours. Jumping by five is unlikely to line up with anything real.\n",
+ "\n",
+ "## Building the whole thing\n",
+ "\n",
+ "Here is the structure, in Python. Two arrays, the probe loop above for lookups, a second probe loop that stops at the first free slot for inserts, and one resize rule.\n",
+ "\n",
+ "The resize rule is two macros. A table is never allowed past two thirds full, [Objects/dictobject.c:581@v3.15.0rc1#USABLE_FRACTION](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L581), and when it fills, the new size is the smallest power of two that fits three times the number of live keys, [Objects/dictobject.c:618-628@v3.15.0rc1#GROWTH_RATE](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L618-L628) and [Objects/dictobject.c:584-604@v3.15.0rc1#calculate_log2_keysize](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L584-L604). Three times rather than two so that a dict which is being deleted from as much as inserted into still has room.\n",
+ "\n",
+ "a dict grows when its entry array is full rather than when its length crosses a threshold, so the resize points fall at 6, 11, 22, 43 and 86 keys"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-10",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "EMPTY = -1\n",
+ "DUMMY = -2\n",
+ "\n",
+ "\n",
+ "def usable(size):\n",
+ " \"\"\"USABLE_FRACTION: how many entries a table of this size may hold.\"\"\"\n",
+ " return (size * 2) // 3\n",
+ "\n",
+ "\n",
+ "def new_log2_size(live_keys):\n",
+ " \"\"\"GROWTH_RATE fed into calculate_log2_keysize: room for three times the live keys.\"\"\"\n",
+ " return (max(live_keys * 3, 8) - 1).bit_length()\n",
+ "\n",
+ "\n",
+ "class Compact:\n",
+ " \"\"\"A dict, in the same shape CPython uses. Enough of one to compare against.\"\"\"\n",
+ "\n",
+ " def __init__(self):\n",
+ " self.indices = [EMPTY] * 8\n",
+ " self.entries = []\n",
+ " self.used = 0\n",
+ "\n",
+ " def _lookup(self, key, key_hash):\n",
+ " for slot in probe_order(key_hash, len(self.indices).bit_length() - 1):\n",
+ " row = self.indices[slot]\n",
+ " if row == EMPTY:\n",
+ " return slot, EMPTY\n",
+ " if row >= 0:\n",
+ " stored_hash, stored_key, _ = self.entries[row]\n",
+ " if stored_hash == key_hash and (stored_key is key or stored_key == key):\n",
+ " return slot, row\n",
+ " raise AssertionError(\"the probe order always finds an empty slot\")\n",
+ "\n",
+ " def _free_slot(self, key_hash):\n",
+ " for slot in probe_order(key_hash, len(self.indices).bit_length() - 1):\n",
+ " if self.indices[slot] < 0:\n",
+ " return slot\n",
+ " raise AssertionError(\"the probe order always finds an empty slot\")\n",
+ "\n",
+ " def _resize(self, log2_size):\n",
+ " self.entries = [row for row in self.entries if row is not None]\n",
+ " self.indices = [EMPTY] * (1 << log2_size)\n",
+ " for row, (stored_hash, _, _) in enumerate(self.entries):\n",
+ " self.indices[self._free_slot(stored_hash)] = row\n",
+ "\n",
+ " def __setitem__(self, key, value):\n",
+ " key_hash = hash(key)\n",
+ " slot, row = self._lookup(key, key_hash)\n",
+ " if row >= 0:\n",
+ " stored_hash, stored_key, _ = self.entries[row]\n",
+ " self.entries[row] = (stored_hash, stored_key, value)\n",
+ " return\n",
+ " if usable(len(self.indices)) - len(self.entries) <= 0:\n",
+ " self._resize(new_log2_size(self.used))\n",
+ " slot = self._free_slot(key_hash)\n",
+ " self.indices[slot] = len(self.entries)\n",
+ " self.entries.append((key_hash, key, value))\n",
+ " self.used += 1\n",
+ "\n",
+ " def __delitem__(self, key):\n",
+ " slot, row = self._lookup(key, hash(key))\n",
+ " if row < 0:\n",
+ " raise KeyError(key)\n",
+ " self.indices[slot] = DUMMY\n",
+ " self.entries[row] = None\n",
+ " self.used -= 1\n",
+ "\n",
+ " def __iter__(self):\n",
+ " return (row[1] for row in self.entries if row is not None)\n",
+ "\n",
+ " def size(self):\n",
+ " return len(self.indices)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-11",
+ "metadata": {},
+ "source": [
+ "Sixty lines, and it should behave like the real thing. The test is not that it looks right, it is that it grows at the same moments and iterates in the same order."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-12",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "mine, real = Compact(), {}\n",
+ "mine_grew, real_grew = [], []\n",
+ "mine_size, real_size = mine.size(), sys.getsizeof(real)\n",
+ "\n",
+ "for n in range(1, 200):\n",
+ " key = f\"k{n}\"\n",
+ " mine[key] = n\n",
+ " real[key] = n\n",
+ " if mine.size() != mine_size:\n",
+ " mine_grew.append(n)\n",
+ " mine_size = mine.size()\n",
+ " if sys.getsizeof(real) != real_size:\n",
+ " real_grew.append(n)\n",
+ " real_size = sys.getsizeof(real)\n",
+ "\n",
+ "print(f\" my table grew at {mine_grew}\")\n",
+ "print(f\" the real one at {real_grew}\")\n",
+ "print(f\" same iteration order {list(mine) == list(real)}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-13",
+ "metadata": {},
+ "source": [
+ "Same growth points and the same order over two hundred keys.\n",
+ "\n",
+ "The real dict has one extra entry in its list, the very first key. An empty dict does not own a keys object at all. It points at a single shared immortal one with no room in it, [Objects/dictobject.c:630-640@v3.15.0rc1#empty_keys_struct](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L630-L640), so the first insert has to allocate. After that the two agree exactly.\n",
+ "\n",
+ "Now delete a few keys and add one, which is the case where a design that moved entries around would fall apart."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-14",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "for key in [\"k3\", \"k17\", \"k50\"]:\n",
+ " del mine[key]\n",
+ " del real[key]\n",
+ "\n",
+ "mine[\"late\"] = 0\n",
+ "real[\"late\"] = 0\n",
+ "\n",
+ "print(f\" same order after deletes {list(mine) == list(real)}\")\n",
+ "print(f\" first six keys {list(mine)[:6]}\")\n",
+ "print(f\" last key {list(mine)[-1]}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-15",
+ "metadata": {},
+ "source": [
+ "## Deleting leaves a mark\n",
+ "\n",
+ "Look at what `__delitem__` did. It wrote `-2` into the index slot and cleared the entry row. It did not shrink either array, and it did not move anything.\n",
+ "\n",
+ "\n",
+ "\n",
+ "The dummy has to be there. A `-1` means \"stop looking\", so turning a deleted slot back into `-1` would cut off the probe sequence for any key that had collided with it and been pushed further along. The C does exactly the same two writes, [Objects/dictobject.c:2904-2941@v3.15.0rc1#delitem_common](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L2904-L2941).\n",
+ "\n",
+ "The entry row is a different matter. Its space is gone until the next resize, and the resize counter never gets it back. So a dict you delete from and insert into in a loop keeps growing even though its length never changes.\n",
+ "\n",
+ "a dict that is deleted from and inserted into in equal measure still resizes, because the entry array only ever gets compacted by a resize"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-16",
+ "metadata": {
+ "cpython_internals": {
+ "varies": "These are byte counts, so they halve on a 32 bit build, which is what a browser gives you. The dict growing while its length stays at five is the part to read."
+ }
+ },
+ "outputs": [],
+ "source": [
+ "churn = {}\n",
+ "for n in range(5):\n",
+ " churn[f\"k{n}\"] = n\n",
+ "\n",
+ "print(f\" five keys {sys.getsizeof(churn):4} bytes\")\n",
+ "\n",
+ "for n in range(5, 10):\n",
+ " del churn[f\"k{n - 5}\"]\n",
+ " churn[f\"k{n}\"] = n\n",
+ "\n",
+ "print(f\" after five swaps {sys.getsizeof(churn):4} bytes, still {len(churn)} keys\")\n",
+ "print(f\" and in order {list(churn)}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-17",
+ "metadata": {},
+ "source": [
+ "> **Version note.** These are byte counts, so they halve on a 32 bit build, which is what a browser gives you. The dict growing while its length stays at five is the part to read."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-18",
+ "metadata": {},
+ "source": [
+ "Five keys in, five out, five in. The length never went above five and the dict is a size class larger than it started. That is the reason `dict.clear()` exists as something other than a loop, and the reason a long lived dict used as a cache is worth rebuilding now and then.\n",
+ "\n",
+ "`popitem()` is the one delete that does give the space back, because it always takes the last row of the entry array and can just decrement the counter."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-19",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "d = dict.fromkeys(\"abcde\")\n",
+ "print(f\" popitem {d.popitem()}\")\n",
+ "print(f\" popitem {d.popitem()}\")\n",
+ "print(f\" left {list(d)}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-20",
+ "metadata": {},
+ "source": [
+ "## Two kinds of entry\n",
+ "\n",
+ "There are two entry layouts, [Include/internal/pycore_dict.h:79-90@v3.15.0rc1#PyDictKeyEntry](https://github.com/python/cpython/blob/v3.15.0rc1/Include/internal/pycore_dict.h#L79-L90). The general one holds a hash, a key and a value. The unicode one holds a key and a value, and no hash.\n",
+ "\n",
+ "\n",
+ "\n",
+ "Dropping the hash is safe when every key is an exact `str`, because a string caches its own hash inside the string object. Storing it again in the entry would be a redundant word per row. So a dict whose keys are all strings uses two words per row instead of three.\n",
+ "\n",
+ "Almost every dict in a running program is that kind. Instance dicts, class dicts, module globals and keyword arguments all have string keys.\n",
+ "\n",
+ "a dict with only exact str keys uses a smaller entry layout, and putting one non string key in converts the whole table to the larger one"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-21",
+ "metadata": {
+ "cpython_internals": {
+ "varies": "Byte counts again, so a 32 bit build halves them. The string keyed dict being the smaller of the two, and jumping when a non string key arrives, holds everywhere."
+ }
+ },
+ "outputs": [],
+ "source": [
+ "strings = {\"a\": 1, \"b\": 2}\n",
+ "numbers = {1: \"a\", 2: \"b\"}\n",
+ "print(f\" two string keys {sys.getsizeof(strings)} bytes\")\n",
+ "print(f\" two int keys {sys.getsizeof(numbers)} bytes\")\n",
+ "\n",
+ "strings[1] = \"and now a number\"\n",
+ "print(f\" after adding one int key to the string dict {sys.getsizeof(strings)} bytes\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-22",
+ "metadata": {},
+ "source": [
+ "> **Version note.** Byte counts again, so a 32 bit build halves them. The string keyed dict being the smaller of the two, and jumping when a non string key arrives, holds everywhere."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-23",
+ "metadata": {},
+ "source": [
+ "The conversion happens inside the resize, [Objects/dictobject.c:2141-2175@v3.15.0rc1#dictresize](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L2141-L2175), which takes a flag saying whether the new table can stay unicode and clears it if the old one was already general.\n",
+ "\n",
+ "## Asking for the space up front\n",
+ "\n",
+ "One last piece. If you know how many keys are coming, there is a reverse of `USABLE_FRACTION` that works out the table size needed to hold them without any resizing, [Objects/dictobject.c:606-615@v3.15.0rc1#estimate_log2_keysize](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/dictobject.c#L606-L615).\n",
+ "\n",
+ "`dict.fromkeys` on something with a known length uses it. Building the same dict a key at a time does not, and goes through every intermediate size on the way. The end result is the same size either way, because the growth rule lands on the same power of two. What differs is the work in between."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "o07-24",
+ "metadata": {
+ "cpython_internals": {
+ "varies": "The two byte counts are smaller on a 32 bit build. That they match each other is the point, and that holds everywhere."
+ }
+ },
+ "outputs": [],
+ "source": [
+ "presized = dict.fromkeys(range(1000))\n",
+ "\n",
+ "built = {}\n",
+ "for n in range(1000):\n",
+ " built[n] = None\n",
+ "\n",
+ "print(f\" presized {sys.getsizeof(presized)} bytes\")\n",
+ "print(f\" built up {sys.getsizeof(built)} bytes\")\n",
+ "print(f\" same size {sys.getsizeof(presized) == sys.getsizeof(built)}\")\n",
+ "print(\" but the second one allocated and rehashed its way through eight smaller tables\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-25",
+ "metadata": {},
+ "source": [
+ "> **Version note.** The two byte counts are smaller on a 32 bit build. That they match each other is the point, and that holds everywhere."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "o07-26",
+ "metadata": {},
+ "source": [
+ "## Try it yourself\n",
+ "\n",
+ "Three things to poke at.\n",
+ "\n",
+ "Give `Compact` a `__getitem__` that counts how many slots the probe loop visited, then fill it with a thousand keys and take the average. It should be close to one. Then swap `probe_order` for plain linear probing, `i = (i + 1) & mask`, fill it with `range(1000)` as keys, and watch the average climb.\n",
+ "\n",
+ "Make the dummy slots break something. Take out the `DUMMY` write in `__delitem__` and put `EMPTY` there instead. It will pass a simple test and then lose a key, and you will need a collision to see it, so start with keys that share a slot.\n",
+ "\n",
+ "Count the resizes. Add a counter to `_resize` and compare building a dict with a loop against `dict.fromkeys` for a few different sizes. The number is small, which is the point of doubling, but it is not zero.\n",
+ "\n",
+ "## What just happened\n",
+ "\n",
+ "A dict is a small array of slot numbers in front of an append only array of entries. The slot array has the holes. The entry array does not, which is why iteration is in insertion order and why that order costs nothing to maintain.\n",
+ "\n",
+ "A lookup masks the hash down to a slot number, reads one small integer, and stops right there if it is `-1`. On a collision it moves on by `i * 5 + perturb + 1`, which visits every slot exactly once and pulls in the high bits of the hash that the mask discarded. For a size 8 table the order is 0, 1, 6, 7, 4, 5, 2, 3.\n",
+ "\n",
+ "The table is never more than two thirds full, and when the entry array fills, it is rebuilt at the smallest power of two that fits three times the live keys. That puts the resize points at 6, 11, 22, 43 and 86 rather than anywhere round. Sixty lines of Python reproduce all of it, including the order after deletes.\n",
+ "\n",
+ "Deleting writes a dummy into the slot array so probe sequences stay intact, and clears the entry row without reclaiming it. Delete and insert in equal measure and the dict still grows. `popitem` is the exception, because it takes the last row.\n",
+ "\n",
+ "A dict with only exact string keys stores no hashes, because strings cache their own, so its rows are two words instead of three. One non string key converts the whole table at the next resize.\n",
+ "\n",
+ "## What is next\n",
+ "\n",
+ "O08 is the layout this lesson left out. Every instance of a class tends to have the same attribute names, so the keys array can be shared between all of them and each instance keeps only a bare array of values. That is a split table, it is why `__dict__` looks like a dict but is not always stored as one, and it is most of the reason ordinary Python objects are as small as they are."
+ ]
+ }
+ ],
+ "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 7594743..c084cc5 100644
--- a/probes/pyodide/lessons.json
+++ b/probes/pyodide/lessons.json
@@ -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 76 b0 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 8c b3 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 1788194934 source 1788194934\n size 54 source 54\n"
+ "printed": " magic 168627755\n flags 0\n mtime 1788195724 source 1788195724\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: 1788194953\n imported it with no complaint: 'hi world'\n and the header now says 1788194953\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: 1788195699\n imported it with no complaint: 'hi world'\n and the header now says 1788195699\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 25455504\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 23942736\n id(Thing) is 23942736\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 24640080\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 25838136\n id(Thing) is 25838136\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",
@@ -998,6 +998,59 @@
}
]
},
+ {
+ "slug": "o07-two-arrays-and-a-hash",
+ "cells": [
+ {
+ "name": "o07-03",
+ "status": "ok"
+ },
+ {
+ "name": "o07-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": "o07-08",
+ "status": "ok",
+ "printed": " starting from a hash of 0 [0, 1, 6, 7, 4, 5, 2, 3, 0]\n every slot exactly once True\n and then it wraps True\n"
+ },
+ {
+ "name": "o07-10",
+ "status": "ok"
+ },
+ {
+ "name": "o07-12",
+ "status": "ok",
+ "printed": " my table grew at [6, 11, 22, 43, 86, 171]\n the real one at [1, 6, 11, 22, 43, 86, 171]\n same iteration order True\n"
+ },
+ {
+ "name": "o07-14",
+ "status": "ok",
+ "printed": " same order after deletes True\n first six keys ['k1', 'k2', 'k4', 'k5', 'k6', 'k7']\n last key late\n"
+ },
+ {
+ "name": "o07-16",
+ "status": "ok",
+ "printed": " five keys 108 bytes\n after five swaps 156 bytes, still 5 keys\n and in order ['k5', 'k6', 'k7', 'k8', 'k9']\n"
+ },
+ {
+ "name": "o07-19",
+ "status": "ok",
+ "printed": " popitem ('e', None)\n popitem ('d', None)\n left ['a', 'b', 'c']\n"
+ },
+ {
+ "name": "o07-21",
+ "status": "ok",
+ "printed": " two string keys 108 bytes\n two int keys 128 bytes\n after adding one int key to the string dict 128 bytes\n"
+ },
+ {
+ "name": "o07-24",
+ "status": "ok",
+ "printed": " presized 20536 bytes\n built up 20536 bytes\n same size True\n but the second one allocated and rehashed its way through eight smaller tables\n"
+ }
+ ]
+ },
{
"slug": "t01-one-line-seven-stages",
"cells": [
@@ -1380,7 +1433,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",
@@ -1657,7 +1710,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",
@@ -1691,12 +1744,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 0x14a7718, 3 reference(s), 26 bytes, not tracked\nlist at 0x16e7408, 2 reference(s), 44 bytes, tracked by the cycle collector\ndict at 0x162e4d0, 2 reference(s), 108 bytes, tracked by the cycle collector\nfunction at 0x1557cf8, 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 0x11417d0, 3 reference(s), 26 bytes, not tracked\nlist at 0x123e4e8, 2 reference(s), 44 bytes, tracked by the cycle collector\ndict at 0x15ff1d0, 2 reference(s), 108 bytes, tracked by the cycle collector\nfunction at 0x1408088, 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) 0x16100a0\nid(b) 0x16e7468\nid(c) 0x16100a0\n"
+ "printed": "a == b True same contents\na is b False different objects\na is c True same object\n\nid(a) 0x12f1cc8\nid(b) 0x1843aa8\nid(c) 0x12f1cc8\n"
},
{
"name": "t08-17",
@@ -1736,7 +1789,7 @@
{
"name": "t08-35",
"status": "ok",
- "printed": "dict at 0x11ef2a0\ndict at 0x1240240\nlist at 0x15a4b80\n"
+ "printed": "dict at 0x12650b8\ndict at 0x12f3c18\nlist at 0x1843990\n"
},
{
"name": "t08-37",
@@ -1795,7 +1848,7 @@
{
"name": "t09-21",
"status": "ok",
- "printed": "Node at 0x16128d0 -> Node at 0x1592680 -> Node at 0x170d880 -> Node at 0x16128d0\n"
+ "printed": "Node at 0x182e450 -> Node at 0x17a01b0 -> Node at 0x18cac60 -> Node at 0x182e450\n"
},
{
"name": "t09-23",
@@ -1825,7 +1878,7 @@
{
"name": "t09-37",
"status": "ok",
- "printed": "first object was at 0x1592330\nsecond object is at 0x1592330\nsame address reused -> True\n"
+ "printed": "first object was at 0x11fabd8\nsecond object is at 0x11fabd8\nsame address reused -> True\n"
}
]
},
diff --git a/probes/pyodide/lessons.md b/probes/pyodide/lessons.md
index 89b193a..fd41478 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.
-34 lesson(s) on Pyodide 3.14.2: 34 ran end to end, 362 cell(s) in total.
+35 lesson(s) on Pyodide 3.14.2: 35 ran end to end, 372 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.
@@ -30,6 +30,7 @@ The checks in `report.md` next to this ask whether a surface exists. This runs t
| o04-the-order-things-are-found-in | 11 | runs end to end |
| o05-what-a-dot-does | 10 | runs end to end |
| o06-what-property-actually-is | 13 | runs end to end |
+| o07-two-arrays-and-a-hash | 10 | 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 056764d..1c0469f 100644
--- a/pyxray/src/pyxray/glossary.py
+++ b/pyxray/src/pyxray/glossary.py
@@ -833,6 +833,23 @@ def anchor(name: str) -> str:
see=("type object", "heap type"),
met="O02",
),
+ Term(
+ name="compact dict",
+ short="A dict laid out as a small array of slot numbers in front of an entry array.",
+ long="The slot array, `dk_indices`, holds a row number into the entry array, or -1 for a position never used, or -2 for one that used to hold something. The entry array is appended to and never reordered, so iterating it top to bottom gives insertion order with no bookkeeping at all. Only the small array has holes, which is why the layout is called compact: a mostly empty hash table costs one byte per slot rather than a whole row.",
+ cite="Include/internal/pycore_dict.h:196-235@v3.15.0rc1#_dictkeysobject",
+ also=("compact ordered dict",),
+ see=("probe sequence", "instance dictionary"),
+ met="O07",
+ ),
+ Term(
+ name="probe sequence",
+ short="The order of slots a dict lookup visits when the first one is a collision.",
+ long="The first slot is the hash masked down to the table size. After that the step is `i = mask & (i * 5 + perturb + 1)`, where `perturb` starts as the whole hash and is shifted right by 5 each round. The `i * 5 + 1` part visits every slot exactly once and in an order unrelated to how consecutive keys arrive, and `perturb` brings back the high bits of the hash the mask discarded. For a size 8 table starting at slot 0 the order is 0, 1, 6, 7, 4, 5, 2, 3.",
+ cite="Objects/dictobject.c:1078-1101@v3.15.0rc1#do_lookup",
+ see=("compact dict",),
+ met="O07",
+ ),
),
)
| |