Deep Engineering

Interview preparation

One mechanism, one lesson, ordered by how often it is asked: decorators, iterators and generators first. Each lesson explains the mechanism from the documentation and the CPython sources — measured, not remembered.

Sections
3
Articles
46
SOURCES
184
Difficulty
46 lessons · matching

Sections

Articles

IntermediatePublishedChannels in Go: a value that bypasses the buffer, a select with no priorities, and a price paid in parkingA channels interview climbs a ladder: what is in the variable — where does the value actually go — what happens with a closed and a nil channel — how does select choose — what does a buffer cost — where do goroutines leak. This lesson climbs all of it: why a waiting receiver gets the value straight past the buffer, why the cancellation branch has no priority in select, and why an unbuffered channel costs three times more — though not because "channels are slow".gochannelsselectgoroutinesconcurrency25 MIN3 SOURCESRead →IntermediatePublishedContext in Go: cancellation only downwards, a cancel that is not about cancelling, and a Value that grows dearer with depthThe interview climbs a ladder: what a context is for — why it is the first argument — what cancel does and why it is always called — how Canceled differs from DeadlineExceeded — what belongs in Value. Measured: a forgotten cancel leaves 123 bytes per child forever, and Value at twenty layers is 18 times dearer.gocontextcancellationtimeoutconcurrency30 MIN4 SOURCESRead →IntermediatePublisheddefer, panic and recover in Go: three moments instead of one, and a recover that silently does nothingThe interview climbs a ladder: when a deferred call runs — when its arguments are evaluated — in what order — why a deferred function can change the result — what recover catches and what it does not — and what all of it costs. Measured: an open-coded defer at 4.68 ns against 16.8 ns per iteration in a loop, and a panic against an error return by a factor of 122.godeferpanicrecoverruntime35 MIN4 SOURCESRead →IntermediatePublishedErrors in Go: the one letter that breaks the chain, and the type assertion that will stop workingThe interview climbs a ladder: what error is — how %w differs from %v — why == stops working after a wrap — how Is differs from As — what Join does — and what it all costs. Measured: errors.Is over a chain of twenty wrappers is 8.2 times dearer, and a wrap itself is two allocations and 162 ns, paid only on the error path.goerrorswrappingerrors-iserrors-as25 MIN4 SOURCESRead →AdvancedPublishedStack, heap and escape analysis: why a pointer means nothing yetWhere a value lives is decided in Go neither by you nor by the presence of &— it is decided by the compiler, and it asks one question: will the value outlive its frame. Measured: a pointer that never leaves gives zero allocations, while a struct with no pointer at all gives one; escaping costs ×12.8, and the price grows with the size of the value.gomemoryescape-analysisperformancecompiler30 MIN3 SOURCESRead →AdvancedPublishedThe garbage collector: why pauses do not grow with the heap, and GOGC speeds up nothingGo's collector is concurrent and non-moving, and everything else follows from that: pauses are short and independent of heap size, and GOGC is not an accelerator but a knob that trades time for memory. Measured: the heap grew sixteenfold while the median pause grew 1.22×, and even that difference is smaller than the spread between two repeats of the same measurement; GOGC=400 gives four times fewer cycles and a two-and-a-half times larger peak.gogcmemoryperformanceruntime25 MIN4 SOURCESRead →IntermediatePublishedInterfaces in Go: two words, a nil that is not nil, and a call four times dearerAn interview on interfaces climbs a ladder: what sits in the variable — why nil is sometimes not nil — when boxing allocates — what a call through an interface costs — how a type assertion works. The lesson climbs all of it, and everything follows from one fact: there are two words, not one.gointerfacesniltype-assertionruntime30 MIN4 SOURCESRead →IntermediatePublishedMaps in Go: a group of eight, the 7/8 threshold, and a miss that costs more than a hitA maps interview climbs a ladder: what is in the variable — how does lookup work — how does it grow — what is the complexity — why is iteration order "random" — what happens on concurrent access. This lesson climbs all of it, showing the mechanism at every rung: why the map keeps an eighth of its slots empty on purpose, why growth happens at 7/8 rather than when full, and why a miss takes longer to look up than a hit.gomapsswiss-tablehashruntime35 MIN3 SOURCESRead →IntermediatePublishedMethod receivers in Go: the copy you cannot see, and the "pointer is faster" that is wrongAn interview on receivers climbs a ladder: what a receiver is — why the change did not stick — why the type "does not implement" the interface — when the compiler takes the address for you and when it refuses — and what the copy costs. The lesson climbs all of it: the measurement shows no difference at all up to eight words, and a tenfold one at a kilobyte.gomethodsreceiversmethod-setsperformance25 MIN3 SOURCESRead →AdvancedPublishedGoroutines and the Go scheduler: two thousand bytes, preemption, and a GOMAXPROCS that limits the wrong thingThe interview climbs a ladder: how a goroutine differs from a thread — what it costs — what GMP is — what GOMAXPROCS limits — whether the scheduler is preemptive — and when a goroutine leaves the CPU. Measured: 2000 bytes of stack plus 500 of runtime structures, a spawn-and-wait 82 times dearer than a call, and yielding the CPU at 106 ns.gogoroutinesschedulergmpgomaxprocs30 MIN4 SOURCESRead →IntermediatePublishedSlices in Go: a window onto somebody else's array, two paths through append, and a growth rule everyone remembers wrongA slice does not own its data: it is a window onto somebody else's array, and one write through one window is visible through the other. The lesson goes from that model to the mechanism — the two paths through append, the real growth formula, what hides behind "amortized O(1)", and why "×2 up to 1024, then ×1.25" is wrong in both numbers.goslicesappendmemoryruntime35 MIN3 SOURCESRead →IntermediatePublishedStrings, runes and bytes: why len does not count characters, and a substring holds megabytesA string in Go is a header of a pointer and a length over immutable bytes. Everything else follows: len gives bytes, s[i] gives a byte, range gives runes at byte offsets, a substring is free and therefore retains the whole original array. Measured: ten bytes hold eight megabytes, and joining a thousand pieces with += costs 109 times a Builder.gostringsunicodeutf-8memory30 MIN4 SOURCESRead →AdvancedPublishedsync and atomic in Go: two pieces of advice that fall apart under measurementThe interview climbs a ladder: what is wrong with a plain increment — how atomic differs from a mutex — when to reach for an RWMutex — what Once does — why go vet catches a copied mutex. Measured: without contention a mutex is twice as dear as an atomic, but as contenders grow it becomes 9.6 times dearer while the atomic barely moves; and on a short section an RWMutex loses to a plain Mutex.gosyncatomicmutexconcurrency30 MIN4 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14*args and **kwargs: two stars with different prices — and one of them makes unknown names acceptable*args is a tuple, **kwargs is a dict, and the resemblance ends there. Passing one star is nearly free; passing two costs four times as much. And a **kwargs added for flexibility turns a misspelled argument name from a TypeError into a silently accepted default — throwing away the hint the interpreter learned to give in 3.13.functionsargumentscpython-internals25 MIN6 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14async/await: the call runs nothing — and three bugs that never crash follow from thatCalling an async function does not run its body: it builds an object and returns. From that follow the forgotten await that turns any check into a truth, the blocking call that stops not one task but the whole event loop and every task on it, and the gather whose second exception stays with its own task and never reaches the caller.asyncioasync-awaitconcurrencycpython-internals40 MIN8 SOURCESRead →IntermediatePublished3.12 · 3.13 · 3.14Closures capture the variable, not the value — and the scope chain walks past the class bodyA closure holds a cell, not a copy: the value is read when the function is called, and one cell is shared by every inner function at once. The familiar “local → enclosing → global → builtins” chain leaves out the two things people actually trip on: a class body is not part of it, and an assignment anywhere in a body makes the name local for the whole body.closuresscopenonlocalcpython-internals35 MIN5 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Comprehensions: the scope boundary runs inside the square brackets, not around themThe loop variable does not leak — everyone knows that. But the first iterable is evaluated outside while the rest runs inside, and in a class body that produces a NameError on a name written one line above. One thing does leak out, though: the walrus.comprehensionsscopewalruscpython-internals35 MIN5 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Decorators: expressions top-down, application bottom-upA decorator is an ordinary call that happens once, the moment def runs. The expressions are read top-down and applied bottom-up, and nearly everything else follows from that pair: why functools.wraps is not politeness, and why @classmethod under someone else's decorator breaks without a sound.decoratorsfunctoolsdescriptorscpython-internals45 MIN9 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Context managers: two methods, one returned truth, and one silent data lossThe protocol is exactly two methods on the type. But the fate of an exception after a successful entry is decided by the truthiness of a single value — the one __exit__ returns. Return something truthy and the exception disappears along with the rest of the block, leaving nothing in the log.context-managerswith-statementcontextlibcpython-internals40 MIN9 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Exceptions and finally: the one word that stops a function from failingA try that does not fire costs about two nanoseconds — five percent of an empty call — and the bytecode explains why. The expensive thing is something else: a return in finally, which throws the exception away in silence and turns the function into one no error can leave.exceptions-finallyexception-groupscpython-internals30 MIN6 SOURCESRead →AdvancedPublished3.11 · 3.12 · 3.13 · 3.14Descriptors: one precedence rule that property, methods and slots all grow out ofA descriptor is an object on the class with __get__, __set__ or __delete__. Everything else follows from a single line: a data descriptor outranks the instance dictionary, a non-data one is outranked by it. That is where a working cached_property comes from — and a failing @classmethod under someone else's decorator.descriptorsattributespropertycached-propertycpython-internals35 MIN6 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14is versus ==: one asks about identity, the other calls a method — and half the famous surprises are about neither“is compares objects, == compares values” is true and useless: it does not answer the question people actually have, which is why 1000 is 1000 comes out True. There are three different answers — the small-integer cache, the compiler merging equal constants, and string interning — and code may rely on none of the three.identityequalityinterningnancpython-internals25 MIN7 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Iterators and generators: one method apart, and one silent bugAn iterable can hand you an iterator; an iterator can hand you the next item. That is the whole difference — one method. And out of it grows a bug that never raises: a function that walks its argument twice gets nothing the second time and returns a wrong answer without a sound.iterators-vs-iterablesgeneratorsitertoolscpython-internals40 MIN8 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14lambda: the same thing as def except for two — and the famous loop bug is neither of themlambda and def share a type, share the bytecode of the body, and have call times measurement cannot tell apart. The difference is the name, and that the body must be an expression. The famous loop bug has nothing to do with lambda: a plain def breaks in exactly the same way.lambdacpython-internals20 MIN6 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14List vs tuple: the 16 bytes you only get from one comparison out of three, and the two operations where there is no difference“A tuple is lighter by 16 bytes” holds only if you compare it with a list built from a literal. Against one grown by append the same difference is 72 bytes — and getsizeof reports that honestly. Meanwhile “tuples are faster” holds for exactly one operation out of five, for a reason that has nothing to do with being a tuple.listtupledata-structurescpython-internals40 MIN6 SOURCESRead →AdvancedPublished3.11 · 3.12 · 3.13 · 3.14Metaclasses: the six steps of class creation, which show what a metaclass is for and what no longer needs oneA metaclass is the type of a class, and the whole protocol follows from that alone. The order of the steps is scattered across three sections of the documentation; collected into one column it answers every practical question at once — including the one that makes most metaclasses unnecessary.metaclasstypeinit-subclassclass-creationcpython-internals25 MIN6 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Mutable default arguments: the value lives in a field of the function, and every call shares itEveryone knows the rule about not writing a list as a default argument, and everyone breaks it anyway, because it reads like an arbitrary prohibition. It stops being arbitrary the moment you see where that value lives: in the function's __defaults__, created once when the def executed.defaultsmutabilityfunctionsdataclasses25 MIN5 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Copying: assignment copies nothing, copy copies one level, deepcopy copies the whole graphThree operations people confuse with each other, because on a flat list all three look the same. The difference shows up only on nesting — and that is also where it turns out a deep copy does not “copy everything”: a shared object stays shared, a cycle stays a cycle, and __init__ is never called.copydeepcopymutabilityreferencescpython-internals25 MIN6 SOURCESRead →IntermediatePublishedThe accept queue: the connection is established and the server knows nothing about itA client connected, sent its request and is waiting. The server has not called `accept` once and has no idea the client exists. Between them sits a kernel queue — and most of what looks like "the network is slow" actually happens in it.srelinuxtcpbacklogaccept20 MIN3 SOURCESRead →IntermediatePublishedcgroups and the OOM killer: who gets killed, why the log is empty, and what 137 really meansThe container is gone, the application log has nothing in it, and the orchestrator reports exit code 137. That is not a mystery but two mechanisms doing their job: memory accounted per group of processes, and a kill by a signal that cannot be caught. The lesson takes both apart and shows by measurement that usage climbed to the limit and stopped there: the kill is not for exceeding the limit but for having nothing left to free inside the group.srelinuxcgroupsmemoryoom25 MIN5 SOURCESRead →IntermediatePublishedConsistent hashing: how many keys move when the membership changes"Hash modulo the number of nodes" works right up to the first change of membership. Computed exactly: adding one node to eight moves 88.9 % of the keys — eight times more than necessary. A ring moves 11.5 %, and removing a node touches no other node's keys. The price is skew: without virtual nodes the busiest node holds ninety times more than the lightest.sreshardinghashingdistributedload-balancing25 MIN1 SOURCERead →IntermediatePublishedGraceful shutdown: where the errors on every rollout come from"A few errors during a deploy" is not a fact of nature but the consequence of one decision: what a process does when it receives SIGTERM. The lesson measures both branches on a real server under load: an immediate exit breaks every request in flight, draining breaks none and costs the remainder of the work already started.sreshutdowndeploysignalsavailability20 MIN2 SOURCESRead →IntermediatePublishedHealth checks: three different questions called by one nameLiveness asks "are you alive", readiness asks "are you ready to take traffic", startup asks "have you finished booting". They get confused, and the price of the confusion is measurable: a probe tied to a shared database turns one database outage into a simultaneous restart of every replica.srehealth-checkslivenessreadinesscascade20 MIN1 SOURCERead →IntermediatePublishedIdempotency: exactly once is about the effect, not the deliveryRetrying a request is not a fix — it is a way to make the effect happen twice. Measured: four retries without a key gave four charges, with a key one. Between the effect and the answer there is a window that cannot be closed: measured, the client sees a failure where the server has already done everything. And the order of working with the key decides it all: claim it before the work and one effect, write it afterwards and two.sreidempotencyretriesdistributedreliability25 MIN1 SOURCERead →IntermediatePublishedToo many open files: whose limit it is, why it is not shared, and what counts as a descriptor"Too many open files" is usually fixed by a restart and a config edit, without anyone asking whose limit ran out. The lesson takes the mechanism apart: two limits instead of one, inheritance at startup, and the fact that a descriptor is not a file but any open object — every network connection included.srelinuxrlimitfile-descriptorsemfile20 MIN4 SOURCESRead →IntermediatePublishedLoad balancing: why two random choices beat round-robinRound-robin hands out requests evenly — which is exactly why it loses as soon as the requests stop being alike. In the model it gave a 582 ms tail where asking two random servers gave 123.6, and asking all sixteen gave 42.9. This lesson is about where that difference comes from and why a uniform benchmark cannot see it.sreload-balancingqueueinglatencynetworking25 MIN1 SOURCERead →IntermediatePublishedName resolution: the application calls getaddrinfo, not DNSBetween the line "DNS was slow" in a postmortem and an actual name server stand a library and three files. Nearly everything the network gets blamed for is decided in them: the answer can arrive without touching the network, there is no cache inside the process, and a name that does not exist costs seconds computed from settings — before the application's own timeout starts counting.srelinuxdnsgetaddrinforesolver30 MIN3 SOURCESRead →IntermediatePublishedPage cache and fsync: why "written" and "will not be lost" are different claims`write` returns before the data reaches the disk: it reaches the kernel's cache. Everything else follows from that — why unflushed data survives a process crash but may be lost when the power goes, why a flush costs tens of times more than a write, and why databases commit transactions in batches.srelinuxfsyncpage-cachedurability25 MIN3 SOURCESRead →IntermediatePublishedQueues and tails: why 19 % more load makes latency four times worseUtilisation went from 0.80 to 0.95 — a rise of nineteen percent. Time in the system rose fourfold. The lesson takes that disproportion apart and, along the way, refutes the received wisdom that "the tail grows faster than the mean": in a pure queue it does not, and what pulls the tail away from the middle is something else.srequeueinglatencypercentilescapacity25 MIN1 SOURCERead →IntermediatePublishedRetries and jitter: why a service falls over a second time at the moment it recoversRetrying looks like the cheapest possible measure and is the simplest way to finish a service off: it adds load exactly when load is hardest to take, and an identical retry schedule gathers a thousand clients into one instant. The lesson takes the mechanism apart on a model: what backoff does about it, what jitter does, and what jitter costs.sreretriesbackoffjitteroverload20 MIN1 SOURCERead →IntermediatePublishedRollouts and rollbacks: why a canary does not help without the right detectorFour rollout strategies against one and the same defect — with an unexpected result: one and the same canary affects twenty-two times fewer requests when the alarm is raised on the error rate. With a detector that waits for two hundred errors it loses to switching all the traffic at once.sredeploycanaryrollbackrelease25 MIN1 SOURCERead →IntermediatePublishedSignals, zombies and PID 1: why a container ignores SIGTERMThe questions come as a ladder: what a zombie is — who adopts an orphan — why a container does not stop at once — what exit code 137 means. The first two rest on a process dying in two separate events; the last two rest on one kernel rule that is almost never said out loud: for the process with ID 1 in its namespace, the default action of a signal is not carried out.srelinuxsignalsprocessescontainers25 MIN4 SOURCESRead →IntermediatePublishedTIME_WAIT and ephemeral ports: the client hits the wall firstTens of thousands of sockets in TIME_WAIT look like a leak, but the state is normal — and it usually gets treated with the wrong tool. Measured: on an ordinary close the sockets go to the side that closes actively; the state lasts a minute; and what the client runs out of is not memory but port numbers. The settings people turn are documented in tcp(7) — and none of them is about the duration.srelinuxtcpnetworkingtime-wait25 MIN3 SOURCESRead →IntermediatePublishedSLOs and error budgets: why "99.9 %" without a window means nothingThe same target means 43 minutes a month and a minute and a half a day. The lesson works through the arithmetic of the window, shows that an invisible incident eats more budget than a loud one, and explains why choosing between a calendar and a rolling window is choosing the rule by which a deployment freeze ends.sresloerror-budgetreliabilitymetrics25 MIN1 SOURCERead →IntermediatePublishedTimeouts and deadlines: the client left, the work carried onA timeout bounds the waiting of whoever waits and says nothing to whoever works. Two things follow, and both are usually discovered in production: a chain of three services with a one-second timeout at every hop can take three seconds, and after the client leaves the whole chain keeps holding connections and workers.sretimeoutsdeadlinesnetworkingcancellation25 MIN2 SOURCESRead →IntermediatePublishedThe TLS handshake: what the first connection pays for"Session resumption saves a round trip" is a TLS 1.2 sentence that people keep saying about 1.3. Measured over a link with a known delay: a full 1.3 handshake costs one round trip, a 1.2 handshake two, and resumption in 1.3 saves none. What it does save shows up in the bytes.sretlsnetworkinglatencyhandshake25 MIN2 SOURCESRead →

Covered by articles

These come up just as often, but a full article covers them — measurements and sources included.

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.

50 · 63 nodes and edges

Edges as a list

No connections: golang-maps, mutable-default-arg