<?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/"><channel><title>python on Kshitij Patil</title><link>https://ksh.fyi/tags/python/</link><description>Recent content in python on Kshitij Patil</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sat, 17 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://ksh.fyi/tags/python/index.xml" rel="self" type="application/rss+xml"/><item><title>A million objects: where do the bytes go?</title><link>https://ksh.fyi/blogs/python-slots/</link><pubDate>Sat, 17 Oct 2026 00:00:00 +0000</pubDate><guid>https://ksh.fyi/blogs/python-slots/</guid><description>&lt;p&gt;Say you&amp;rsquo;re simulating a forest: a million trees, each with a position and an
age.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three small ints per tree. In C that&amp;rsquo;s 12–24 bytes a tree, so a million trees
would take about 24 MB. In Python 3.10 the same million trees take &lt;strong&gt;152 MB&lt;/strong&gt;.
In 3.13 they take 96 MB, without changing a line.&lt;/p&gt;
&lt;p&gt;There are two ways to make a million objects cheap: &lt;strong&gt;make each object
smaller&lt;/strong&gt;, or &lt;strong&gt;make fewer objects&lt;/strong&gt;. This post is about the first one. It
covers where those bytes go, what &lt;code&gt;__slots__&lt;/code&gt; removes, and how much of that
CPython 3.11+ now does for you.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Say you&rsquo;re simulating a forest: a million trees, each with a position and an
age.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Tree</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">age</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="n">age</span>
</span></span></code></pre></div><p>Three small ints per tree. In C that&rsquo;s 12–24 bytes a tree, so a million trees
would take about 24 MB. In Python 3.10 the same million trees take <strong>152 MB</strong>.
In 3.13 they take 96 MB, without changing a line.</p>
<p>There are two ways to make a million objects cheap: <strong>make each object
smaller</strong>, or <strong>make fewer objects</strong>. This post is about the first one. It
covers where those bytes go, what <code>__slots__</code> removes, and how much of that
CPython 3.11+ now does for you.</p>
<h2 id="how-we-measure">How we measure</h2>
<p><code>sys.getsizeof</code> won&rsquo;t do. It reports the size of the object struct only. On
3.10 that leaves out the instance&rsquo;s <code>__dict__</code>, and on 3.13 it doesn&rsquo;t even
count the attribute values. It says a 3.13 <code>Tree</code> is 48 bytes when it really
costs 96.</p>
<p>Instead, create a million instances and ask <code>tracemalloc</code> how much memory
appeared:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">objs</span> <span class="o">=</span> <span class="p">[</span><span class="kc">None</span><span class="p">]</span> <span class="o">*</span> <span class="n">N</span>                    <span class="c1"># allocate the list first so it isn&#39;t counted</span>
</span></span><span class="line"><span class="cl"><span class="n">tracemalloc</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">before</span> <span class="o">=</span> <span class="n">tracemalloc</span><span class="o">.</span><span class="n">get_traced_memory</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">objs</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">Tree</span><span class="p">(</span><span class="n">i</span> <span class="o">%</span> <span class="mi">100</span><span class="p">,</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">200</span><span class="p">,</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">50</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">per_obj</span> <span class="o">=</span> <span class="p">(</span><span class="n">tracemalloc</span><span class="o">.</span><span class="n">get_traced_memory</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span> <span class="o">-</span> <span class="n">before</span><span class="p">)</span> <span class="o">/</span> <span class="n">N</span>
</span></span></code></pre></div><p>The attribute values are small ints. CPython keeps one shared copy of each int
from -5 to 256, so creating trees allocates no ints and we measure only the
<code>Tree</code> itself.</p>
<p>All numbers below come from <a href="measure/"><code>measure.py</code></a>, which is worth opening
in another tab to follow along. They were measured with 64-bit CPython on macOS
arm64, using <code>uv</code>-managed builds of 3.10 to 3.14. The memory numbers come out
identical on every run. The timings are rough (±10%). 3.8 behaves exactly like
3.10, so it&rsquo;s left out of the tables.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">uv run --python 3.13 measure.py
</span></span></code></pre></div><h2 id="a-plain-class">A plain class</h2>
<h3 id="the-dict-youd-expect">The dict you&rsquo;d expect</h3>
<p>Ask most Python programmers where <code>tree.x</code> lives and they&rsquo;ll say &ldquo;in
<code>tree.__dict__</code>&rdquo;. Every object carries a dict mapping attribute names to
values. Taken literally, that means every tree has a complete dict of its own:
a hash table holding the keys <code>'x'</code>, <code>'y'</code> and <code>'age'</code>, their hashes, and the
values.</p>
<p>You can build exactly that by assigning <code>__dict__</code> yourself:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TreeOwnDict</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">age</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="vm">__dict__</span> <span class="o">=</span> <span class="p">{</span><span class="s1">&#39;x&#39;</span><span class="p">:</span> <span class="n">x</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">:</span> <span class="n">y</span><span class="p">,</span> <span class="s1">&#39;age&#39;</span><span class="p">:</span> <span class="n">age</span><span class="p">}</span>
</span></span></code></pre></div><p>On 3.10 that costs <strong>280 B</strong> per tree, 280 MB for the forest. It&rsquo;s also how
every Python object worked until 3.2.</p>
<h3 id="the-dict-you-actually-get">The dict you actually get</h3>
<p>A normal <code>Tree</code> on 3.10 costs <strong>152 B</strong>. The difference is that every tree has
the same three attribute names, so since 3.3
(<a href="https://peps.python.org/pep-0412/">PEP 412</a>, &ldquo;key-sharing dictionaries&rdquo;),
CPython stores the names once per class and keeps only the values per
instance:</p>
<pre tabindex="0"><code>Tree instance, 48 B
┌───────────────────────┐
│ GC header        16 B │
│ refcount          8 B │
│ type              8 B │──► Tree class
│ __dict__          8 B │──┐   │ ht_cached_keys
│ __weakref__       8 B │  │   ▼
└───────────────────────┘  │   shared keys: x, y, age
                           │   ▲
dict object, 64 B          │   │
┌───────────────────────┐  │   │
│ GC header        16 B │◄─┘   │
│ refcount, type   16 B │      │
│ used, version    16 B │      │
│ keys pointer      8 B │──────┘
│ values pointer    8 B │──┐
└───────────────────────┘  │
                           │
values array, 40 B         │
┌───────────────────────┐  │
│ →1 │ →2 │ →3 │ · │ ·  │◄─┘
└───────────────────────┘
</code></pre><p>Going through it:</p>
<ul>
<li><strong>The instance (48 B)</strong> is all bookkeeping. There&rsquo;s a GC header so the cycle
collector can track the object, a refcount, a pointer to its class, and two
more pointers:
<ul>
<li><code>__dict__</code> points to the instance&rsquo;s dict.</li>
<li><code>__weakref__</code> heads the list of weak references to this tree
(<code>weakref.ref</code>, <code>WeakValueDictionary</code> and friends). It stays NULL until
someone makes one, but every instance pays 8 B for the possibility.</li>
</ul>
</li>
<li><strong>The dict object (64 B)</strong> is mostly header too. Its two interesting fields
are the keys pointer and the values pointer, <code>ma_keys</code> and <code>ma_values</code> in
CPython&rsquo;s source (<code>ma_</code> is a leftover from when dicts were called &ldquo;mapping
objects&rdquo;).</li>
<li><strong>The values array (40 B)</strong> is a separate allocation with room for five
values, three of them used. It doesn&rsquo;t hold the ints themselves: each entry
is a pointer to an int object somewhere else on the heap. That&rsquo;s true of
every attribute in every layout in this post.</li>
<li><strong>The keys table</strong> lives on the class, in a field called <code>ht_cached_keys</code>
(<code>ht</code> for heap type, CPython&rsquo;s name for a class created at runtime). Every
instance&rsquo;s dict points at that one table, so it costs nothing per tree.</li>
</ul>
<p>48 + 64 + 40 = <strong>152 B per tree</strong>. Sharing keys saved 128 B on every object,
because the part every tree has in common is stored once and only the part
that differs is stored per tree. Which raises a question: if the class already
knows the attribute names, why does each instance need a dict at all?</p>
<h2 id="__slots__-no-dict-at-all"><code>__slots__</code>: no dict at all</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Tree</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">(</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">,</span> <span class="s1">&#39;age&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">age</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="n">age</span>
</span></span></code></pre></div><p><code>__slots__</code> tells Python the complete list of attributes up front. It then
stores the attributes&rsquo; pointers straight in the object at fixed offsets, with
no <code>__dict__</code> and no <code>__weakref__</code>. Here&rsquo;s <code>Tree(1, 2, 3)</code>:</p>
<pre tabindex="0"><code>Tree instance, 56 B          offset
┌───────────────────────┐
│ GC header        16 B │     -16
├───────────────────────┤ ◄── id(tree) points here
│ refcount          8 B │       0
│ type              8 B │       8  ──► Tree class
│ age               8 B │      16  ──► 3
│ x                 8 B │      24  ──► 1
│ y                 8 B │      32  ──► 2
└───────────────────────┘
</code></pre><p>Two details hide in there:</p>
<ul>
<li><strong>The GC header sits in front of the object.</strong> <code>id(tree)</code> and every pointer
to the tree point <em>past</em> it, so CPython counts offsets from the refcount.
From the start of the allocation, <code>x</code> is at byte 40.</li>
<li><strong>The slots are sorted.</strong> CPython sorts the names when it creates the class,
so <code>age</code> comes first, whatever order you wrote them in.</li>
</ul>
<p>Finding an attribute is the class&rsquo;s job now. <code>Tree.x</code> is a <code>member_descriptor</code>
that knows &ldquo;x is at offset 24&rdquo;, so <code>tree.x</code> reads the pointer at that offset
and follows it.
The class holds the layout and the instance holds only values. That&rsquo;s the same
split key-sharing makes, taken all the way.</p>
<p><strong>56 B on every version</strong> we tested. On 3.10 that&rsquo;s a million trees in 56 MB
instead of 152 MB, and reads are about 10% faster.</p>
<p>What it costs you:</p>
<ul>
<li><strong>No new attributes.</strong> <code>tree.tag = 1</code> raises <code>AttributeError: 'Tree' object has no attribute 'tag' and no __dict__ for setting new attributes</code>.</li>
<li><strong>No weak references.</strong> <code>weakref.ref(tree)</code> raises <code>TypeError: cannot create weak reference to 'Tree' object</code>. You can buy the pointer back by adding
<code>'__weakref__'</code> to <code>__slots__</code>.</li>
<li><strong>Subclasses must declare <code>__slots__</code> too.</strong> A subclass without them quietly
gets a <code>__dict__</code> back, and with it the cost of a plain class.</li>
<li><strong>No class-level defaults.</strong> <code>x = 0</code> next to <code>__slots__ = ('x',)</code> raises
<code>ValueError: 'x' in __slots__ conflicts with class variable</code>.</li>
<li><strong>Multiple inheritance gets awkward.</strong> Two bases that both have non-empty
slots can&rsquo;t be combined: <code>multiple bases have instance lay-out conflict</code>.</li>
<li><strong><code>functools.cached_property</code> doesn&rsquo;t work</strong>: it needs a <code>__dict__</code> to cache
into.</li>
</ul>
<p>Pickling, <code>copy</code> and <code>dataclasses</code> all handle slots fine.</p>
<h2 id="what-311-does-for-you">What 3.11+ does for you</h2>
<p>Here&rsquo;s the same million trees across versions, in bytes per object:</p>
<table>
	<thead>
			<tr>
					<th>variant</th>
					<th style="text-align: right">3.10</th>
					<th style="text-align: right">3.11</th>
					<th style="text-align: right">3.12</th>
					<th style="text-align: right">3.13</th>
					<th style="text-align: right">3.14</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>own dict (<code>TreeOwnDict</code>)</td>
					<td style="text-align: right">280</td>
					<td style="text-align: right">240</td>
					<td style="text-align: right">232</td>
					<td style="text-align: right">256</td>
					<td style="text-align: right">264</td>
			</tr>
			<tr>
					<td>plain class</td>
					<td style="text-align: right">152</td>
					<td style="text-align: right">96</td>
					<td style="text-align: right">88</td>
					<td style="text-align: right">96</td>
					<td style="text-align: right">96</td>
			</tr>
			<tr>
					<td><code>__slots__</code></td>
					<td style="text-align: right">56</td>
					<td style="text-align: right">56</td>
					<td style="text-align: right">56</td>
					<td style="text-align: right">56</td>
					<td style="text-align: right">56</td>
			</tr>
	</tbody>
</table>
<p>and nanoseconds per attribute read:</p>
<table>
	<thead>
			<tr>
					<th>variant</th>
					<th style="text-align: right">3.10</th>
					<th style="text-align: right">3.11</th>
					<th style="text-align: right">3.12</th>
					<th style="text-align: right">3.13</th>
					<th style="text-align: right">3.14</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>plain class</td>
					<td style="text-align: right">13.2</td>
					<td style="text-align: right">5.4</td>
					<td style="text-align: right">4.3</td>
					<td style="text-align: right">4.7</td>
					<td style="text-align: right">4.5</td>
			</tr>
			<tr>
					<td><code>__slots__</code></td>
					<td style="text-align: right">11.8</td>
					<td style="text-align: right">4.9</td>
					<td style="text-align: right">4.2</td>
					<td style="text-align: right">4.5</td>
					<td style="text-align: right">4.9</td>
			</tr>
	</tbody>
</table>
<p>Three changes produced this.</p>
<p><strong>3.11: no dict until someone asks for one.</strong> A plain object no longer gets a
dict object at creation. The values array hangs directly off the object,
through two hidden pointers placed in front of the refcount, next to the GC
header (together, the &ldquo;pre-header&rdquo;). A real dict is built only if code reads
<code>obj.__dict__</code>. Most code never does, so the 64 B dict object is simply gone:
152 → 96 B.</p>
<p><strong>3.12: one pointer doing two jobs.</strong> The 3.11 pre-header holds two pointers,
one to the values array and one to the dict, but only one of them is ever in
use: the values pointer while there&rsquo;s no dict, the dict pointer once it&rsquo;s been
built. So 3.12 keeps a single pointer and uses its lowest bit to say which kind
it is. That bit is free because objects are always at least 8-byte aligned,
so a real pointer to one always ends in zero bits. The freed word takes the
<code>__weakref__</code> pointer, which moves out of the object, and every tree loses 8 B:
88 B.</p>
<p><strong>3.13: values inside the object.</strong> The values array is no longer a separate
allocation. It&rsquo;s laid out inline, right after the type pointer, which is why
<code>getsizeof</code> stops counting it:</p>
<pre tabindex="0"><code>Tree instance, 96 B, one allocation (3.13)
┌───────────────────────┐
│ GC header        16 B │
│ __weakref__       8 B │  ← pre-header
│ __dict__          8 B │  ← NULL until someone reads it
├───────────────────────┤ ◄── id(tree) points here
│ refcount          8 B │
│ type              8 B │
│ values           48 B │  ← pointers to 1, 2, 3, and room to grow
└───────────────────────┘
</code></pre><p>The total is back to 96 B, because the inline area is sized for the class&rsquo;s
shared keys, with room to spare. But it&rsquo;s one allocation instead of two, and
the values sit right next to the header in memory.</p>
<p><strong>And the speed.</strong> 3.11&rsquo;s specializing interpreter
(<a href="https://peps.python.org/pep-0659/">PEP 659</a>) watches each attribute-access
instruction. When it keeps seeing the same class, it rewrites the instruction
into a version that skips the lookup and reads the value at the known index.
Slots get the same treatment. Plain-class reads became about 2.5× faster than
3.10, and they&rsquo;re now within noise of slots.</p>
<p>So on 3.11+ the plain class has closed most of the memory gap and all of the
speed gap, and it kept every feature <code>__slots__</code> takes away. What&rsquo;s left is
<strong>96 vs 56 B</strong>: slots are still about 40% smaller.</p>
<h2 id="where-the-fast-path-ends">Where the fast path ends</h2>
<p>&ldquo;For free&rdquo; holds as long as your objects look the way CPython expects. Two
things break it.</p>
<h3 id="reading-__dict__">Reading <code>__dict__</code></h3>
<p><code>vars(obj)</code>, <code>obj.__dict__</code>, and any serializer or debugger that does it for
you, build the dict that 3.11+ skipped. Each object you touch gains a 64 B dict
object:</p>
<table>
	<thead>
			<tr>
					<th>bytes per object</th>
					<th style="text-align: right">3.10</th>
					<th style="text-align: right">3.11</th>
					<th style="text-align: right">3.12</th>
					<th style="text-align: right">3.13</th>
					<th style="text-align: right">3.14</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>plain class</td>
					<td style="text-align: right">152</td>
					<td style="text-align: right">96</td>
					<td style="text-align: right">88</td>
					<td style="text-align: right">96</td>
					<td style="text-align: right">96</td>
			</tr>
			<tr>
					<td>plain, after <code>obj.__dict__</code> read</td>
					<td style="text-align: right">152</td>
					<td style="text-align: right">160</td>
					<td style="text-align: right">152</td>
					<td style="text-align: right">160</td>
					<td style="text-align: right">160</td>
			</tr>
	</tbody>
</table>
<p>Worse, on 3.11 and 3.12 its attribute reads stay slow afterwards:</p>
<table>
	<thead>
			<tr>
					<th>ns per attribute read</th>
					<th style="text-align: right">3.10</th>
					<th style="text-align: right">3.11</th>
					<th style="text-align: right">3.12</th>
					<th style="text-align: right">3.13</th>
					<th style="text-align: right">3.14</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>plain class</td>
					<td style="text-align: right">13.2</td>
					<td style="text-align: right">5.4</td>
					<td style="text-align: right">4.3</td>
					<td style="text-align: right">4.7</td>
					<td style="text-align: right">4.5</td>
			</tr>
			<tr>
					<td>plain, after <code>obj.__dict__</code> read</td>
					<td style="text-align: right">13.1</td>
					<td style="text-align: right">16.3</td>
					<td style="text-align: right">16.4</td>
					<td style="text-align: right">4.7</td>
					<td style="text-align: right">4.6</td>
			</tr>
	</tbody>
</table>
<p>That&rsquo;s about 3× slower than before the read on 3.11 and 3.12, slower even than
3.10, because the fast instruction only works while there&rsquo;s no dict, and the
interpreter falls back to a slower path through the dict. 3.13 fixed the speed
(the new dict just points at the inline values) but not the memory.</p>
<h3 id="wide-classes">Wide classes</h3>
<p>The keys table on the class has a fixed maximum size. Once a class sets more
attributes than fit, every instance falls back to a full dict of its own:</p>
<table>
	<thead>
			<tr>
					<th>bytes per object</th>
					<th style="text-align: right">3.10</th>
					<th style="text-align: right">3.11</th>
					<th style="text-align: right">3.12</th>
					<th style="text-align: right">3.13</th>
					<th style="text-align: right">3.14</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>29 attributes</td>
					<td style="text-align: right">448</td>
					<td style="text-align: right">320</td>
					<td style="text-align: right">312</td>
					<td style="text-align: right">320</td>
					<td style="text-align: right">320</td>
			</tr>
			<tr>
					<td>30 attributes</td>
					<td style="text-align: right">448</td>
					<td style="text-align: right">1640</td>
					<td style="text-align: right">1632</td>
					<td style="text-align: right">1904</td>
					<td style="text-align: right">1904</td>
			</tr>
			<tr>
					<td>30 attributes, <code>__slots__</code></td>
					<td style="text-align: right">272</td>
					<td style="text-align: right">272</td>
					<td style="text-align: right">272</td>
					<td style="text-align: right">272</td>
					<td style="text-align: right">272</td>
			</tr>
	</tbody>
</table>
<p>One more attribute and each instance costs five to six times as much, far worse
than 3.10 handled the same class. Slots don&rsquo;t care: 272 B either way.</p>
<p>Thirty sounds like a lot, but it&rsquo;s easy to reach: an ORM row for a wide table,
a settings or config object, a dataclass mirroring an API response or a CSV
record with one attribute per column, a game entity with position, velocity,
health, inventory and a dozen flags.</p>
<h2 id="so-should-you-still-use-__slots__">So, should you still use <code>__slots__</code>?</h2>
<p>On 3.11+, for most classes, no. A plain class is about as fast, costs 96 B
instead of 56 B, and keeps dynamic attributes, weakrefs, defaults,
<code>cached_property</code>, and easy multiple inheritance.</p>
<p>Reach for <code>__slots__</code> when:</p>
<ul>
<li>you really have millions of small objects of the same shape, and 40% of their
memory matters,</li>
<li>the class is wide (30+ attributes), or</li>
<li>something reads <code>__dict__</code> on your hot objects and you&rsquo;re stuck on 3.11/3.12.</li>
</ul>
<p>If you&rsquo;re using dataclasses, it&rsquo;s one word: <code>@dataclass(slots=True)</code> (3.10+)
generates the <code>__slots__</code> for you.</p>
<h2 id="or-dont-make-objects-at-all">Or don&rsquo;t make objects at all</h2>
<p>Even a slots tree is mostly overhead, and its attributes are still pointers.
They have to be: a Python attribute can hold any object, so it stores a
pointer to one. If every <code>x</code> is just an int, you can skip the objects and keep
one NumPy array per attribute, with the raw ints packed side by side:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">i</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">1_000_000</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">age</span> <span class="o">=</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">100</span><span class="p">,</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">200</span><span class="p">,</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">50</span>
</span></span><span class="line"><span class="cl"><span class="n">age</span> <span class="o">+=</span> <span class="mi">1</span>        <span class="c1"># every tree gets a year older</span>
</span></span></code></pre></div><p>That&rsquo;s <strong>24 B per tree</strong>, the 24 MB from the start. Whole-column operations
also run in C:</p>
<table>
	<thead>
			<tr>
					<th>age every tree, 3.13</th>
					<th style="text-align: right">ns per tree</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>plain class, Python loop</td>
					<td style="text-align: right">14.6</td>
			</tr>
			<tr>
					<td><code>__slots__</code>, Python loop</td>
					<td style="text-align: right">13.5</td>
			</tr>
			<tr>
					<td>NumPy, <code>age += 1</code></td>
					<td style="text-align: right">0.20</td>
			</tr>
	</tbody>
</table>
<p>The catch: reading one value from Python with <code>age[k]</code> creates a new Python
object each time, about 8× slower than <code>tree.age</code>, and there&rsquo;s no <code>Tree</code> left
to put methods on. Columns win when you work on the whole forest at once.
Pandas applies the same idea to tables.</p>
<h2 id="whats-next">What&rsquo;s next</h2>
<p>Everything here makes each tree smaller, or stops making trees. The remaining
way, making fewer objects, is the flyweight pattern. That&rsquo;s part 2.</p>
<h2 id="see-it-yourself">See it yourself</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">uv run --python 3.10 measure.py
</span></span><span class="line"><span class="cl">uv run --python 3.13 measure.py
</span></span></code></pre></div><p>The full script: <a href="measure/">measure.py</a> (<a href="measure.py">raw</a>). It declares NumPy
as a dependency at the top, and <code>uv run</code> installs it for you.</p>
<p>The slot layout, read straight out of memory:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="kn">import</span> <span class="nn">ctypes</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="k">class</span> <span class="nc">Tree</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>     <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">(</span><span class="s1">&#39;x&#39;</span><span class="p">,</span> <span class="s1">&#39;y&#39;</span><span class="p">,</span> <span class="s1">&#39;age&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">t</span> <span class="o">=</span> <span class="n">Tree</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">t</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">t</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="n">t</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="s1">&#39;X&#39;</span><span class="p">,</span> <span class="s1">&#39;Y&#39;</span><span class="p">,</span> <span class="s1">&#39;AGE&#39;</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="p">[</span><span class="n">ctypes</span><span class="o">.</span><span class="n">py_object</span><span class="o">.</span><span class="n">from_address</span><span class="p">(</span><span class="nb">id</span><span class="p">(</span><span class="n">t</span><span class="p">)</span> <span class="o">+</span> <span class="n">off</span><span class="p">)</span><span class="o">.</span><span class="n">value</span> <span class="k">for</span> <span class="n">off</span> <span class="ow">in</span> <span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">24</span><span class="p">,</span> <span class="mi">32</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="s1">&#39;AGE&#39;</span><span class="p">,</span> <span class="s1">&#39;X&#39;</span><span class="p">,</span> <span class="s1">&#39;Y&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">Tree</span><span class="o">.</span><span class="n">__basicsize__</span>          <span class="c1"># refcount + type + 3 slots; getsizeof adds the 16 B GC header</span>
</span></span><span class="line"><span class="cl"><span class="mi">40</span>
</span></span></code></pre></div><p>References:
<a href="https://peps.python.org/pep-0412/">PEP 412</a> (key-sharing dictionaries),
<a href="https://peps.python.org/pep-0659/">PEP 659</a> (specializing adaptive interpreter),
<a href="https://docs.python.org/3/reference/datamodel.html#slots"><code>__slots__</code> in the data model</a>,
CPython&rsquo;s <a href="https://github.com/python/cpython/blob/main/Objects/object_layout.md">object layout notes</a>.</p>
]]></content:encoded></item></channel></rss>