Deep Engineering

Python Deep Dive

How CPython actually runs your code. Every article is written against a pinned interpreter tag and says outright which behaviour is a language guarantee and which is an implementation detail.

Sections
5
Articles
14
SOURCES
115
Difficulty
14 articles · matching

Sections

Articles

AdvancedPublished3.13Moving to async: the connection pool sets the ceiling, not the execution modelA thousand coroutines against a pool of eight connections is eight concurrent queries and a queue of 992. Three execution models on the same workload land within 5 % of each other, while on a short query with no concurrency the async code costs 16–33 % more than the synchronous one.asynciopostgresqlpsycopgasyncpgconnection-pool25 MIN5 SOURCESRead →AdvancedPublished3.12 · 3.13 · 3.14The event loop: one thread, one queue, and six steps on repeatThe event loop is a plain while True with six steps inside it. How many times it repeats is set by suspensions, not by tasks: swap ten coroutines for a thousand and the count does not move. That is where the classic trap comes from: one time.sleep stalls the whole service.asyncioevent-loopconcurrencycoroutinescpython-internals30 MIN10 SOURCESRead →ExpertPublished3.13 · 3.14 · 3.15GIL: not “Python can't do threads,” but “a thread waits exactly as long as you asked”The switch interval is a setting that works: the median wait follows it, and at 5 and 50 ms it is off by a few percent. Until the case arrives where it does not work at all — and that case is measured in hundreds of milliseconds.gilthreadingfree-threadingconcurrencycpython-internals35 MIN10 SOURCESRead →InternalsPublished3.6 · 3.13Python dictionaries from the inside: the CPython hash tableHashing, open addressing, and the 2016 rewrite that made insertion order a side effect of the implementation — and, one release later, a guarantee of the language.dicthash-tablecpython-internalsdata-structures45 MIN12 SOURCESRead →AdvancedPublished3.12 · 3.13 · 3.14Lists from the inside: growth, shrinking, and the price of the frontA list has two lengths, and the second one has no public name in Python. Everything else follows from it: why lists that look identical take different amounts of memory, why append is cheap while insert(0) is quadratic, and why from 3.13 onwards del L[0] and L.pop(0) leave the object in different states — at the same length of a hundred elements the list takes 8056 bytes instead of 1528.cpython-internalsdata-structureslistmemoryperformance35 MIN7 SOURCESRead →AdvancedPublished3.12 · 3.13 · 3.14Strings from the inside: three widths, four layout cases, interning, and the price of concatenationA string's object size at the same length is set by the highest character code in it: one Cyrillic letter among Latin ones doubles the room for every character, one emoji quadruples it. Two other questions follow from the same place: why `is` sometimes works for strings, and why `s += x` in a loop is sometimes linear and sometimes quadratic — the fast path has two conditions, and the second one is invisible in the code.cpython-internalsdata-structuresstringsunicodememory30 MIN5 SOURCESRead →ExpertPublished3.11 · 3.12 · 3.13 · 3.14Memory management in CPython: three numbers everyone repeats wrongGarbage collector thresholds, arena size, the cost of an object — none of it is what the articles say any more. Every number here was taken off a running interpreter and checked against the source at a pinned tag.memorygcrefcountpymallocfree-threading40 MIN13 SOURCESRead →AdvancedPublished3.11 · 3.12 · 3.13 · 3.14__slots__: what it changes in the object model and what it costs in bytesThe mechanism first: the declared names get data descriptors on the class and places at fixed offsets, while __dict__ and __weakref__ stop being created automatically. All of that is the language contract, identical on 3.11–3.14. Only then the price, which is not part of the contract: 40 bytes per instance, 42% — while sys.getsizeof reports the opposite, basicsize stays silent about __weakref__, and the sum of two getsizeof calls answers differently at the start of a program than later on.cpython-internalsmemoryslotsobject-layoutdescriptors35 MIN5 SOURCESRead →ExpertPublished3.11 · 3.12 · 3.13 · 3.14The adaptive interpreter: how CPython rewrites itself as it goesOn the second execution of an instruction the interpreter swaps it for a narrow version fitted to the types it saw — and keeps a counter and a cache inside the bytecode itself to check the assumption still holds. That is where the 53% penalty for a line five types pass through comes from, and why switching the 3.14 JIT on lost on all four of the short loops it was tried on.cpython-internalsspecializationinline-cachejitbytecode20 MIN7 SOURCESRead →ExpertPublished3.11 · 3.12 · 3.13 · 3.14What happens on o.x: descriptors, the MRO and the type cacheA dot is not a field read. It is a protocol of four mechanisms, and one line of precedence orders them. From that line follow why assigning to a property without a setter raises AttributeError, why __getattr__ sometimes never fires, and why reaching an inherited attribute does not depend on how deep the inheritance goes — as long as the type version cache holds. And on 3.14 there turns out to be a way to switch that cache off without knowing it exists.cpython-internalsdescriptorsmrotype-cacheattribute-access35 MIN7 SOURCESRead →ExpertPublished3.12 · 3.13 · 3.14Calling a function: what it costs, what does not happen, and where cells liveThree common claims about the cost of a call — a method costs more than a function, binding a method in advance is cheaper, staticmethod is faster — the measurement confirms none of them and contradicts two. The reason is visible in the bytecode: on the fast path no bound-method object is created at all. That same fact opens the second half of the article — closures: the variable a nested function sees is stored through a separate object — a cell the frame only points at — and everything else follows from that.cpython-internalsruntimecallsclosuresspecialization35 MIN6 SOURCESRead →ExpertPublished3.12 · 3.13 · 3.14The interpreter loop: where a frame lives and how control is dispatchedThe bytecode is ready — who runs it? One C loop, where a call frame is not a separate heap object but a chunk on the thread's data stack, and the next instruction is chosen in one of three ways depending on how Python was built. From this come both the recursion limit and the fact that `f_locals` stopped being a snapshot in 3.13.cpython-internalseval-loopframesdispatchbytecode15 MIN8 SOURCESRead →AdvancedPublished3.12 · 3.13 · 3.14From source to bytecode: the six steps Python takes before the first instructionOne line of code becomes fourteen tokens, six instructions and sixteen cells. Ten of those sixteen hold no code at all — they are room for a cache that dis never shows you. Everything else grows out of that arithmetic: specialisation, the attribute cache, and the caret under the exact sub-expression in a traceback.bytecodecompilercode-objectastcpython-internals25 MIN11 SOURCESRead →AdvancedPublished3.12 · 3.13 · 3.14Typing decorators: why the checker goes quiet, and what fixes itA decorator declared as Callable[..., R] lets both deliberately wrong calls through — and that is not the type checker failing, it is exactly what the ellipsis means. Meanwhile the signature is still there at runtime: inspect shows the original one. The runtime is honest and the checker is blind — and people usually expect the opposite.typingparamspecconcatenatedecoratorsprotocol25 MIN9 SOURCESRead →

Recommended order

Knowledge graph

NON-LINEAR · PREREQUISITE AWARE

Topics are connected, and the connections here were not drawn by hand — they were counted: every article states what it builds on and what it sits next to, and the picture is assembled from those statements. A dashed circle is a topic the articles already call related but that has not been written yet.

16 · 26 nodes and edges

Edges as a list