<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Dict on Opensource internals</title><link>https://articles.9j.nz/tags/dict/</link><description>Posts filed under Dict on Opensource internals.</description><generator>Hugo</generator><language>en-US</language><managingEditor>Jay Hubbert</managingEditor><lastBuildDate>Tue, 20 Oct 2026 09:00:00 +0000</lastBuildDate><atom:link href="https://articles.9j.nz/tags/dict/index.xml" rel="self" type="application/rss+xml"/><item><title>CPython's dict Implementation in C: Combined Tables, Split Keys, and the Probe Sequence</title><link>https://articles.9j.nz/posts/how-cpython-dict-is-implemented/</link><pubDate>Tue, 20 Oct 2026 09:00:00 +0000</pubDate><dc:creator>Jay Hubbert</dc:creator><guid isPermaLink="true">https://articles.9j.nz/posts/how-cpython-dict-is-implemented/</guid><category>python</category><category>cpython</category><category>internals</category><category>dict</category><description>A guided tour of CPython 3.15's dict implementation: PyDictObject, PyDictKeysObject, open addressing with perturb, combined vs split tables, shared keys, resizing at 2/3 load, insertion order, and free-threaded safety.</description><content:encoded>&lt;blockquote&gt;&#10;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; CPython&amp;rsquo;s &lt;code&gt;dict&lt;/code&gt; is a hash table split into a &lt;code&gt;PyDictObject&lt;/code&gt; header and a &lt;code&gt;PyDictKeysObject&lt;/code&gt; that holds the actual table. It uses open addressing with a perturb-based probe sequence, resizes at 2/3 load, preserves insertion order, and can share keys across instances (split tables) — a huge win for object &lt;code&gt;__dict__&lt;/code&gt;s. Here&amp;rsquo;s how it all fits together in CPython 3.15.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Python&amp;rsquo;s &lt;code&gt;dict&lt;/code&gt; is a hash table with a twist: since 3.6 it&amp;rsquo;s compact and preserves insertion order, and since 3.3 a single keys object can be &lt;em&gt;shared&lt;/em&gt; by many instances of the same class. In CPython this lives in &lt;code&gt;Objects/dictobject.c&lt;/code&gt;. Let&amp;rsquo;s trace the representation and the hot paths through the 3.15 source.&lt;/p&gt;&#10;&lt;h2 id="the-object-struct"&gt;The object struct&lt;/h2&gt;&#10;&lt;p&gt;A &lt;code&gt;dict&lt;/code&gt; is split into two pieces: the &lt;code&gt;PyDictObject&lt;/code&gt; header and a &lt;code&gt;PyDictKeysObject&lt;/code&gt; that holds the actual hash table:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Include/cpython/dictobject.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyObject_HEAD&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Py_ssize_t&lt;/span&gt; &lt;span class="n"&gt;ma_used&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* number of items */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint64_t&lt;/span&gt; &lt;span class="n"&gt;_ma_watcher_tag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* watcher bits + mutation counter + unique id */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyDictKeysObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ma_keys&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* the hash table */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyDictValues&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ma_values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* NULL =&amp;gt; combined table */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;PyDictObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;ma_values&lt;/code&gt; is the switch between the two table kinds. When it&amp;rsquo;s &lt;code&gt;NULL&lt;/code&gt;, the dict is &lt;strong&gt;combined&lt;/strong&gt;: keys and values both live inside &lt;code&gt;ma_keys&lt;/code&gt;. When it&amp;rsquo;s non-NULL, the dict is &lt;strong&gt;split&lt;/strong&gt;: keys live in the shared &lt;code&gt;ma_keys&lt;/code&gt;, values in a per-instance &lt;code&gt;ma_values&lt;/code&gt; array.&lt;/p&gt;&#10;&lt;h2 id="pydictkeysobject"&gt;PyDictKeysObject&lt;/h2&gt;&#10;&lt;p&gt;The keys object is the hash table. Its header is followed in memory by &lt;code&gt;dk_indices&lt;/code&gt; (the probe array) and then &lt;code&gt;dk_entries&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Include/internal/pycore_dict.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;_dictkeysobject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Py_ssize_t&lt;/span&gt; &lt;span class="n"&gt;dk_refcnt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;dk_log2_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* log2(table size), power of two */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;dk_log2_index_bytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 1/2/4/8 byte indices */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;dk_kind&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* GENERAL / UNICODE / SPLIT */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;threaded&lt;/span&gt; &lt;span class="n"&gt;builds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;dk_version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Py_ssize_t&lt;/span&gt; &lt;span class="n"&gt;dk_usable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* free slots left */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Py_ssize_t&lt;/span&gt; &lt;span class="n"&gt;dk_nentries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* used entries in dk_entries */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;dk_indices&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="cm"&gt;/* the hash table */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cm"&gt;/* dk_entries[] follows */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;dk_kind&lt;/code&gt; distinguishes general tables (arbitrary keys, &lt;code&gt;PyDictKeyEntry&lt;/code&gt; with cached hash) from Unicode-keyed tables (&lt;code&gt;PyDictUnicodeEntry&lt;/code&gt;, no cached hash needed because Unicode objects cache their own hash). The third kind, &lt;code&gt;DICT_KEYS_SPLIT&lt;/code&gt;, is the shared-keys case.&lt;/p&gt;&#10;&lt;p&gt;Two entry types:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Include/internal/pycore_dict.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Py_hash_t&lt;/span&gt; &lt;span class="n"&gt;me_hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;me_key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;me_value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* meaningful only for combined tables */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;PyDictKeyEntry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;me_key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* must be exact Unicode with cached hash */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;me_value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* meaningful only for combined tables */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;PyDictUnicodeEntry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="combined-vs-split-tables"&gt;Combined vs. split tables&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Combined table&lt;/strong&gt; (&lt;code&gt;ma_values == NULL&lt;/code&gt;): each entry stores key, hash, and value together in &lt;code&gt;dk_entries&lt;/code&gt;. &lt;code&gt;dk_refcnt == 1&lt;/code&gt;. This is the default for ordinary dicts.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Split table&lt;/strong&gt; (&lt;code&gt;ma_values != NULL&lt;/code&gt;): keys are stored once in a shared &lt;code&gt;PyDictKeysObject&lt;/code&gt; (marked &lt;code&gt;DICT_KEYS_SPLIT&lt;/code&gt;), values in a per-instance &lt;code&gt;PyDictValues&lt;/code&gt;. Only Unicode keys are allowed. This is the optimization for instance &lt;code&gt;__dict__&lt;/code&gt;: thousands of objects of the same class share one keys table, and each instance only pays for its own value array.&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Include/internal/pycore_dict.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define _PyDict_HasSplitTable(d) ((d)-&amp;gt;ma_values != NULL)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="memory-layout"&gt;Memory layout&lt;/h2&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;text&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;pre tabindex="0"&gt;&lt;code&gt;PyDictObject (GC-tracked)&#10;+---------------------------+&#10;| ob_refcnt / ob_type |&#10;+---------------------------+&#10;| ma_used |&#10;+---------------------------+&#10;| _ma_watcher_tag |&#10;+---------------------------+&#10;| *ma_keys ----------------+--&amp;gt; PyDictKeysObject (PyMem)&#10;| *ma_values (or NULL) | +---------------------------+&#10;+---------------------------+ | dk_refcnt |&#10; | dk_log2_size |&#10; | dk_log2_index_bytes |&#10; | dk_kind |&#10; | dk_version |&#10; | dk_usable |&#10; | dk_nentries |&#10; +---------------------------+&#10; | dk_indices[] | &amp;lt;- hash table&#10; | int8/16/32/64 per slot |&#10; +---------------------------+&#10; | dk_entries[] | &amp;lt;- key/value rows&#10; | (USABLE_FRACTION(size)) |&#10; +---------------------------+&#10;&#10;If split:&#10;PyDictValues (PyMem)&#10;+---------------------------+&#10;| capacity / size / embedded / valid |&#10;+---------------------------+&#10;| values[capacity] |&#10;+---------------------------+&#10;| insertion-order byte array |&#10;+---------------------------+&lt;/code&gt;&lt;/pre&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The index array uses the smallest integer width that fits the table size (1 byte up to 128 slots, 2 bytes up to 2^15, etc.), saving memory for the common small dictionaries.&lt;/p&gt;&#10;&lt;h2 id="hash-probing"&gt;Hash probing&lt;/h2&gt;&#10;&lt;p&gt;The hash table is open-addressed with the classic CPython probe sequence. The initial slot is &lt;code&gt;hash &amp;amp; mask&lt;/code&gt; (mask = size-1, size is a power of two). On collision:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Objects/dictobject.c (do_lookup)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dictkeys_get_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ix&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;compare&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ix&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DKIX_EMPTY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DKIX_EMPTY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;perturb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;PERTURB_SHIFT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* PERTURB_SHIFT == 5 */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;perturb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;DKIX_EMPTY&lt;/code&gt; is -1, &lt;code&gt;DKIX_DUMMY&lt;/code&gt; is -2. A dummy slot is a tombstone: it can&amp;rsquo;t be turned back into &amp;ldquo;empty&amp;rdquo; without breaking probe chains, so deletions leave dummies behind. Empty slots terminate a search. The &lt;code&gt;perturb&lt;/code&gt; variable folds the high bits of the hash into later probes, so even pathological key sets (e.g. &lt;code&gt;i &amp;lt;&amp;lt; 16&lt;/code&gt;) eventually spread out.&lt;/p&gt;&#10;&lt;h2 id="deletion-and-tombstones"&gt;Deletion and tombstones&lt;/h2&gt;&#10;&lt;p&gt;Deleting a key doesn&amp;rsquo;t remove its entry from &lt;code&gt;dk_entries&lt;/code&gt; (which would break insertion order cheaply); instead the index slot is overwritten with &lt;code&gt;DKIX_DUMMY&lt;/code&gt; (-2). The entry row is set to &lt;code&gt;NULL&lt;/code&gt; key/value so its references can be dropped. Dummies can&amp;rsquo;t be reused in free-threaded builds (to keep lock-free reads safe), so deletion-heavy tables accumulate tombstones and are compacted by the next resize, which copies only live entries.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;popitem()&lt;/code&gt; is the special case: it removes the last entry outright, decrementing &lt;code&gt;dk_nentries&lt;/code&gt;, giving amortized O(1) removal from the end while leaving a dummy behind in the index array.&lt;/p&gt;&#10;&lt;h2 id="insertion-path"&gt;Insertion path&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;PyDict_SetItem&lt;/code&gt; ultimately calls &lt;code&gt;insertdict&lt;/code&gt;. For an empty dict it takes the fast &lt;code&gt;insert_to_emptydict&lt;/code&gt; path; otherwise it looks up the key, and on a miss calls &lt;code&gt;insert_combined_dict&lt;/code&gt;, which resizes if &lt;code&gt;dk_usable&lt;/code&gt; is exhausted and then writes the new entry at the found slot:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Objects/dictobject.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old_value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insert_combined_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;STORE_USED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ma_used&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;For split tables, &lt;code&gt;insert_split_key&lt;/code&gt; records the key in the shared keys (if room remains) and &lt;code&gt;_PyDict_InsertSplitValue&lt;/code&gt; stores the value in the instance&amp;rsquo;s slot; once the shared table is full the dict is converted to a combined table.&lt;/p&gt;&#10;&lt;h2 id="load-factor-and-resizing"&gt;Load factor and resizing&lt;/h2&gt;&#10;&lt;p&gt;The table is resized when it reaches two-thirds full:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Objects/dictobject.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define USABLE_FRACTION(n) (((n) &amp;lt;&amp;lt; 1)/3) &lt;/span&gt;&lt;span class="cm"&gt;/* 2/3 */&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;dk_usable&lt;/code&gt; counts free slots; when it hits zero a resize is triggered. Growth size is &lt;code&gt;used * 3&lt;/code&gt; (so the table roughly doubles when there are few deletions, but has more headroom otherwise):&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Objects/dictobject.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define GROWTH_RATE(d) ((d)-&amp;gt;ma_used*3)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;dictresize&lt;/code&gt; allocates a fresh &lt;code&gt;PyDictKeysObject&lt;/code&gt; and re-inserts all entries, rebuilding indices with &lt;code&gt;build_indices_generic&lt;/code&gt; / &lt;code&gt;build_indices_unicode&lt;/code&gt;. After a resize the table is always combined, even if it was split before (split values are copied in and the old value array freed). Deletion-heavy tables may actually shrink.&lt;/p&gt;&#10;&lt;h2 id="insertion-order"&gt;Insertion order&lt;/h2&gt;&#10;&lt;p&gt;Combined tables preserve order trivially: &lt;code&gt;dk_entries&lt;/code&gt; is append-only, so iterating entries yields insertion order. The exception is &lt;code&gt;popitem()&lt;/code&gt;, which removes the last entry and decrements &lt;code&gt;dk_nentries&lt;/code&gt;, leaving a dummy in the index array.&lt;/p&gt;&#10;&lt;p&gt;Split tables record insertion order in a byte array stored after the value array:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Include/internal/pycore_dict.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;get_insertion_order_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PyDictValues&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;When a key is inserted, its entry index is appended to this byte array (&lt;code&gt;_PyDictValues_AddToInsertionOrder&lt;/code&gt;). With capacity capped at &lt;code&gt;SHARED_KEYS_MAX_SIZE&lt;/code&gt; (30), the shared-keys table is limited to 30 entries.&lt;/p&gt;&#10;&lt;h2 id="shared-keys-the-split-dict-optimization"&gt;Shared keys (the split dict optimization)&lt;/h2&gt;&#10;&lt;p&gt;When a class&amp;rsquo;s instances get attributes, CPython creates the instance &lt;code&gt;__dict__&lt;/code&gt; as a split dict sharing one keys object:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Objects/dictobject.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;PyObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;new_dict_with_shared_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PyDictKeysObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shared_keys_usable_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyDictValues&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;new_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;dictkeys_incref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;new_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;ht_cached_keys&lt;/code&gt; on the type holds the shared keys. When an attribute is set, &lt;code&gt;insert_split_key&lt;/code&gt; records the key in the shared table (if not already present) and &lt;code&gt;_PyDict_InsertSplitValue&lt;/code&gt; stores the value in the instance&amp;rsquo;s value array. This turns per-instance dict memory from &amp;ldquo;full hash table&amp;rdquo; into &amp;ldquo;array of values&amp;rdquo; — a big win for objects with many attributes.&lt;/p&gt;&#10;&lt;h2 id="iteration-and-views"&gt;Iteration and views&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;dict.keys()&lt;/code&gt;, &lt;code&gt;.values()&lt;/code&gt;, &lt;code&gt;.items()&lt;/code&gt; return view objects that hold a reference to the underlying dict and iterate over &lt;code&gt;dk_entries&lt;/code&gt; in order. Because the entries array is append-only for live inserts, iteration is O(n) with no hashing. Mutating the dict during iteration raises &lt;code&gt;RuntimeError: dictionary changed size during iteration&lt;/code&gt; (detected via &lt;code&gt;ma_used&lt;/code&gt;/&lt;code&gt;dk_nentries&lt;/code&gt; changes).&lt;/p&gt;&#10;&lt;h2 id="version-numbers-and-watchers"&gt;Version numbers and watchers&lt;/h2&gt;&#10;&lt;p&gt;Every keys object carries a &lt;code&gt;dk_version&lt;/code&gt; (reset to 0 on any key change) and the dict carries a &lt;code&gt;_ma_watcher_tag&lt;/code&gt;. The version lets the specializing interpreter and JIT cache &amp;ldquo;this dict has these keys&amp;rdquo; and invalidate cheaply when keys change. Watchers (registered via &lt;code&gt;PyDict_AddWatcher&lt;/code&gt;) receive &lt;code&gt;ADDED&lt;/code&gt;/&lt;code&gt;MODIFIED&lt;/code&gt;/&lt;code&gt;DELETED&lt;/code&gt;/&lt;code&gt;CLONED&lt;/code&gt;/&lt;code&gt;CLEARED&lt;/code&gt;/&lt;code&gt;DEALLOCATED&lt;/code&gt; events, used by the adaptive optimizer and debuggers.&lt;/p&gt;&#10;&lt;h2 id="inline-values-managed-dicts"&gt;Inline values (managed dicts)&lt;/h2&gt;&#10;&lt;p&gt;For small instance dicts, CPython can store values &lt;em&gt;inside&lt;/em&gt; the object&amp;rsquo;s memory rather than in a separate &lt;code&gt;PyDictValues&lt;/code&gt; allocation. The type&amp;rsquo;s &lt;code&gt;tp_basicsize&lt;/code&gt;/&lt;code&gt;tp_dictoffset&lt;/code&gt; machinery plus &lt;code&gt;_PyInlineValuesSize&lt;/code&gt; computes a slot count from the shared keys&amp;rsquo; usable size; a prefix byte array records which slots are populated and in what insertion order. &lt;code&gt;_PyObject_MaterializeManagedDict&lt;/code&gt; converts such an inline dict into a real &lt;code&gt;PyDictObject&lt;/code&gt; on demand (e.g. when &lt;code&gt;__dict__&lt;/code&gt; is accessed).&lt;/p&gt;&#10;&lt;h2 id="free-lists"&gt;Free lists&lt;/h2&gt;&#10;&lt;p&gt;Both &lt;code&gt;PyDictObject&lt;/code&gt; and &lt;code&gt;PyDictKeysObject&lt;/code&gt; are recycled:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Objects/dictobject.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PyDictObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_Py_FREELIST_POP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PyDictObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dicts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PyDictKeysObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;dk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_Py_FREELIST_POP_MEM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dictkeys&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cm"&gt;/* only minsize unicode */&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The dict object freelist and the keys freelist (only for the minimum-size Unicode case) avoid repeated &lt;code&gt;PyMem_Malloc&lt;/code&gt;/&lt;code&gt;PyObject_GC_New&lt;/code&gt; traffic. &lt;code&gt;PyDict_Clear&lt;/code&gt; resets a dict to &lt;code&gt;Py_EMPTY_KEYS&lt;/code&gt; rather than freeing it.&lt;/p&gt;&#10;&lt;h2 id="allocation-fast-path-for-empty-dicts"&gt;Allocation fast path for empty dicts&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;PyDict_New&lt;/code&gt; starts from the immortal &lt;code&gt;Py_EMPTY_KEYS&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Objects/dictobject.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PyObject&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;PyDict_New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;new_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Py_EMPTY_KEYS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The first insertion into an empty dict (&lt;code&gt;insert_to_emptydict&lt;/code&gt;) allocates a fresh minimum-size keys object and stores the single entry directly at &lt;code&gt;hash &amp;amp; (MINSIZE-1)&lt;/code&gt;, avoiding a full resize.&lt;/p&gt;&#10;&lt;h2 id="frozendict"&gt;Frozendict&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;frozendict&lt;/code&gt; (new in 3.15) subclasses &lt;code&gt;PyDictObject&lt;/code&gt; adding a cached &lt;code&gt;ma_hash&lt;/code&gt;. It&amp;rsquo;s created untracked by the GC until finalized, and its keys object is immutable in practice: mutations are forbidden, so the hash can be computed once and cached:&lt;/p&gt;&#10;&lt;div class="code-block"&gt;&#10; &lt;div class="code-block-head"&gt;&#10; &lt;span class="code-lang"&gt;c&lt;/span&gt;&#10; &lt;button class="code-copy" type="button" data-copy&gt;Copy&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;div class="code-block-body"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Include/internal/pycore_dict.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PyDictObject&lt;/span&gt; &lt;span class="n"&gt;ob_base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Py_hash_t&lt;/span&gt; &lt;span class="n"&gt;ma_hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;PyFrozenDictObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="free-threading-notes"&gt;Free-threading notes&lt;/h2&gt;&#10;&lt;p&gt;In &lt;code&gt;Py_GIL_DISABLED&lt;/code&gt; builds, &lt;code&gt;dk_indices&lt;/code&gt; updates use atomic stores (&lt;code&gt;STORE_INDEX&lt;/code&gt;), split keys are protected by &lt;code&gt;dk_mutex&lt;/code&gt;, and reads can proceed lock-free using consume/acquire loads. Dicts are marked &amp;ldquo;shared&amp;rdquo; on first cross-thread access so that resizes delay freeing old keys/values via QSBR, keeping concurrent readers safe.&lt;/p&gt;&#10;&lt;h2 id="key-takeaways"&gt;Key takeaways&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;dict&lt;/code&gt; is &lt;code&gt;PyObject_HEAD&lt;/code&gt; + &lt;code&gt;ma_used&lt;/code&gt;, watcher tag, &lt;code&gt;ma_keys&lt;/code&gt;, and &lt;code&gt;ma_values&lt;/code&gt;; &lt;code&gt;ma_values == NULL&lt;/code&gt; means combined.&lt;/li&gt;&#10;&lt;li&gt;The hash table lives in &lt;code&gt;PyDictKeysObject&lt;/code&gt;: an index array (&lt;code&gt;dk_indices&lt;/code&gt;) of int8/16/32/64 slots plus an append-only entry array (&lt;code&gt;dk_entries&lt;/code&gt;).&lt;/li&gt;&#10;&lt;li&gt;Lookups use open addressing with probe &lt;code&gt;i = (5*i + 1 + perturb) &amp;amp; mask&lt;/code&gt;, &lt;code&gt;perturb &amp;gt;&amp;gt;= 5&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Tables resize at 2/3 load; growth is &lt;code&gt;used*3&lt;/code&gt;; entries are re-inserted.&lt;/li&gt;&#10;&lt;li&gt;Split tables share keys across instances (instance &lt;code&gt;__dict__&lt;/code&gt;), storing values in a compact per-instance array with an insertion-order tail.&lt;/li&gt;&#10;&lt;li&gt;Insertion order comes from append-only entries (combined) or the order byte vector (split).&lt;/li&gt;&#10;&lt;li&gt;Dicts and minimum-size Unicode keys are freelist-recycled.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="further-reading"&gt;Further reading&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;Objects/dictobject.c&lt;/code&gt; — all of the above.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Include/cpython/dictobject.h&lt;/code&gt; — &lt;code&gt;PyDictObject&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Include/internal/pycore_dict.h&lt;/code&gt; — &lt;code&gt;PyDictKeysObject&lt;/code&gt;, entries, macros.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Objects/dictnotes.txt&lt;/code&gt; — design notes and tuning.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</content:encoded></item></channel></rss>