Deep Engineering

Interview preparation

One mechanism, one lesson. The Python track starts with the foundation — from classes and collections to tests, threads and environments — and then follows how often a topic is asked: decorators, descriptors, iterators and generators. The Go track also starts with the foundation — from structs and pointers to modules and concurrency patterns. Each lesson explains the mechanism from the documentation and the sources — measured, not remembered.

Sections
3
Articles
71
SOURCES
340
Difficulty
71 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 →IntermediatePublishedFunctions and closures in Go: what gets captured and whenInterview questions on closures climb a ladder: can two functions be compared — what a closure is — what a counter prints — what closures created in a loop see — when defer arguments are evaluated and what a method value saves — does f(s...) pass a copy — what a closure costs. A run answers each step: a closure reads an assignment made after it was created; the same loop gave c c c in a module with go 1.21 and a b c with go 1.22; f(s...) wrote into the caller's slice; and calling a closure cost as much as a direct call — only creating one is expensive.gofunctions-closuresclosuresloop-variablevariadic55 MIN8 SOURCESRead →IntermediatePublishedConcurrency patterns in Go: worker pool, pipeline, fan-inInterview questions on patterns climb a ladder: write a worker pool — how many workers to take — in what order the results arrive — who closes the channel — how to merge several channels into one — how to limit parallelism — how to stop a pipeline if the consumer has left. A run answers each step: on two cores eight workers compute no faster than two, while on a sleeping task they are eight times faster than one; a stage that forgot close crashes a small program and silently hangs one where even a single timer is alive; and a consumer that left without cancelling leaves two goroutines behind forever.goconcurrency-patternsworker-poolpipelinefan-in65 MIN6 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 →AdvancedPublishedData races in Go: what breaks, what -race sees and what it does notInterview questions on races climb a ladder: what a data race is — how it differs from a race condition — how to find one — will -race find every race — can you turn it on in production. A run answers each step: a two-word struct without synchronisation hands out pairs nobody ever wrote; -race finds a race that did not spoil the result and misses one that never executed; and a check and a withdrawal under separate mutex acquisitions drive the balance negative in 936 cases out of 1000 with zero reports.godata-racesrace-detectormemory-modelconcurrency30 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 →IntermediatePublishedStruct embedding in Go: composition that gets mistaken for inheritanceInterview questions on embedding climb a ladder: does Go have inheritance — what happens if the outer type declares the same method — why a struct with an embedded type does not implement an interface — what to do with two identical names — why embed an interface. A run answers each step: a promoted method calls its own type's method, not the outer one; two same-named fields silently vanish from JSON; and in a measurement the promoted call is indistinguishable from a direct one.goembeddingcompositionmethod-setsjson30 MIN3 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.gogcmemoryperformanceruntime30 MIN6 SOURCESRead →IntermediatePublishedThe HTTP server in Go: what net/http does by itself and what it does not do until askedInterview questions on the HTTP server climb a ladder: how to build a web server — how it serves concurrent requests — which timeouts to set — what to do when the client has gone — how to stop the server without cutting requests. A run answers each step: every connection gets its own goroutine and there is no built-in limit; http.Server{} has all four timeouts at zero, and a silent client holds a connection for as long as it likes; the request context is cancelled but the work is not; and Serve returns right after Shutdown is called, while requests are still in flight.gonet-httphttp-servergraceful-shutdowntimeouts30 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-assertionruntime35 MIN4 SOURCESRead →IntermediatePublishedio.Reader and io.Writer: a contract of two methodsInterview questions on io climb a ladder: what a Reader is — how to write a read loop — why Read returned less than asked — how io.Copy copies and how much memory it takes — why bufio and what happens without Flush — what is wrong with defer f.Close(). A run answers each step: a loop that looks at the error before the bytes loses the last chunk; io.Copy sends 100,000 bytes in a single Write if the source has WriterTo; and a function with bufio and defer f.Close() returns nil and leaves an empty file on disk.goio-reader-writeriobufioio-copy50 MIN6 SOURCESRead →IntermediatePublishedJSON in Go: tags, omitempty and what encoding/json does silentlyInterview questions on encoding/json (v1) climb a ladder: which fields end up in JSON — what counts as empty for omitempty — why the key NAME filled the field name — where the extra key went — why an id from the database arrived one less — how to write your own MarshalJSON without bringing the process down. A run answers each step: an empty struct and a zero time.Time pass straight through omitempty; Unmarshal ignores case and silently skips what is extra; 9007199254740993 in an any becomes 9007199254740992; a MarshalJSON on a pointer is not called for a value without an address; and in a measurement on go1.24.7, linux/amd64, Marshal of a struct cost less than twice as much as assembling the same string by hand — the expensive side was Unmarshal.gojson-encodingencoding-jsonstruct-tagsomitempty65 MIN8 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-tablehashruntime40 MIN4 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 →IntermediatePublishedGo modules and dependencies: go.mod, go.sum and minimal version selectionInterview questions on modules climb a ladder: what a module is — which version go picks when two libraries require different ones — what go get and go mod tidy do — why go.sum — why the /v2 import — how to swap a dependency for a local copy — what the go line in go.mod means. A run answers each step: a asked for c v1.1.0, b for v1.3.0, the proxy held v1.9.0, and v1.3.0 was built — for a too; go refused to build an archive of the same version replaced in the proxy, printing SECURITY ERROR; and a replace in a dependency's go.mod did not work at all — the folder it pointed to did not exist, and that was not an error.gogo-modulesgo-modgo-summvs60 MIN12 SOURCESRead →IntermediatePublishedPackages in Go: visibility, init and project layoutInterview questions on packages climb a ladder: how to make a name private — what a package is and what the directory has to do with it — why internal — in what order init runs — why an import with an underscore — how to lay out a project. A run answers each step: the variable declared first is initialized last; package alpha is initialized before zeta although main imports zeta first; an import from internal is forbidden not by the compiler but by the go command; and one and the same visibility violation gets two different compiler messages.gopackagespackages-layoutexported-namesinit50 MIN6 SOURCESRead →IntermediatePublishedPointers in Go: everything is passed by value, even a pointerInterview questions on pointers climb a ladder: what a pointer is — is anything in Go passed by reference — what happens after p = nil inside a function — can you call a method on nil — why a map element has no address — can you return a pointer to a local variable — is there pointer arithmetic. A run answers each step: assigning to a pointer parameter is not visible outside, a method on a nil receiver ran fine, a pointer to a slice element writes into the old array after append, and a function that returned a pointer to its own variable cost more than copying a kilobyte in a measurement on go1.24.7.gopointerspass-by-valueniladdressability55 MIN10 SOURCESRead →AdvancedPublishedProfiling in Go: what pprof shows and what it does not show by defaultInterview questions on profiling climb a ladder: how do you find what is slow — how do you take a profile — how flat differs from cum — how to hunt a memory leak — why the block profile is empty — can you profile in production. A run answers each step: the CPU profile is 100 samples a second; a function that allocated almost 600 MB is absent from the in-use memory profile; the block and mutex profiles are empty under real contention until you turn them on; and the cost of the CPU profile is within noise.goprofilingpprofperformancegoroutine-leaks30 MIN4 SOURCESRead →IntermediatePublishedReflection in Go: reflect.Type, reflect.Value and the settability ruleInterview questions on reflection climb a ladder: what reflect.Type and reflect.Value are — how Type differs from Kind — why SetInt panics — whether a private field can be changed — how encoding/json learns names from tags — how DeepEqual differs from == — what all of it costs. A run answers each step: ValueOf sees a copy, and that copy cannot be changed; for Celsius the Type is main.Celsius while the Kind is int; an unexported field can be read but not changed; a nil slice is not equal to an empty one even for DeepEqual; and in a measurement on go1.24.7 looking a field up by name cost more than ten times as much as writing through a ready index.goreflectionreflectstruct-tagsdeepequal50 MIN6 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 an immutable sequence of bytes, and it need not hold valid UTF-8. Hence len gives bytes, s[i] gives a byte, and range decodes UTF-8 into runes at byte offsets. The rest is implementation: in gc a string is a pointer and a length, and a substring copies nothing and therefore retains the whole original array. Measured: ten bytes hold eight megabytes, and joining a thousand pieces with += costs 111.8 times a Builder.gostringsunicodeutf-8memory30 MIN4 SOURCESRead →IntermediatePublishedStructs and methods in Go: a value that is copied wholeInterview questions on structs climb a ladder: does a struct need a constructor — what happens after b := a — can two structs be compared and can a struct be a map key — how much does it weigh — on which type can a method be declared. A run answers each step: after b := a an edit through a slice field shows up in a, while an edit to an array field does not; == on a struct with a slice does not compile, and behind an interface it panics; struct{ a bool; b int64; c bool } weighs 24 bytes on linux/amd64, and with int64 first, 16; and in a measurement on go1.24.7 a map[string]struct{} set of a million keys weighs as much as map[string]bool — 56 MB.gostructs-methodsstructszero-valuememory-layout55 MIN7 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 →IntermediatePublishedTesting in Go: what t.Run, t.Parallel and a benchmark really doInterview questions on tests climb a ladder: what a table-driven test is — how to run one case — why t.Parallel and what breaks with it — why a repeated go test does not run the tests — what is wrong with a benchmark. A run answers each step: the parent's defer closes a resource before the parallel subtests start; in a module with go 1.21 every subtest sees the last case of the table; four sleeping subtests run in pairs because -parallel equals GOMAXPROCS; and a benchmark that discarded the result of a pure inlined function measured an empty loop.gotestingtable-driven-testst-parallelbenchmarks35 MIN6 SOURCESRead →IntermediatePublishedGo tooling: build, vet, fmt and what they checkInterview questions on Go tooling climb a ladder: how go run differs from go build — why the second build is instant — why gofmt — what go vet catches if the code compiles — how to build a Windows binary on Linux — how to embed a version into it. A run answers each step: a cold build called the compiler 55 times, a repeated one not once; a Printf with the wrong verb and a copied sync.Mutex built without a single error, and go vet caught both; go test let the copied mutex through, because it runs only part of vet's checks; and -ldflags -X silently ignored a typo in the variable name.gotoolinggo-vetgofmtbuild-cache45 MIN11 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.11 · 3.12 · 3.13 · 3.14Built-in collections: list, tuple, dict and set — what to use whenThe question of whether to choose a list, a tuple, a dictionary or a set sounds like a matter of taste, but it is settled by three checks: do you need order, do you need repeats, and how will you search. A run answers with numbers: the check x in on a list of a thousand elements is 216 times slower than on a set, and on ten elements only 3.4 times (CPython 3.13.7, one machine); a dictionary keeps insertion order, while a set of strings prints differently under different PYTHONHASHSEED values; and a set of a thousand numbers is 4.1 times bigger than a list.choosing-collectionscollections-modulehashing30 MIN6 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Classes and OOP: self is an argument, and a class attribute is shared by allThe first OOP questions sound simple: what self is, how a class attribute differs from an instance attribute, why classmethod and staticmethod exist, what __init__ does. A run answers each one: a.hello() and Greeter.hello(a) are the same call; a list declared in the class body is one for all instances; __init__ receives an object that already exists; and a class with __eq__ but no __hash__ can no longer be a dictionary key or a set element.oop-inheritancemethods-selfdunder-methodsclassesself30 MIN5 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.14Complexity of operations: O-notation, counted on built-in types"Estimate the complexity of this code" is a question answered with the letter O and checked by counting. A run counts comparisons directly: x in on a list of a hundred thousand elements made a hundred thousand comparisons, and on a set without hash collisions — one, with one hash for all — on the order of n; bisect — seventeen; over a hundred thousand appends the list reallocated its memory 66 times; a list comprehension with x in on another list made a million comparisons at a thousand elements; and sorted() on already sorted data makes exactly n − 1.algorithms-complexitybig-osortingbisect25 MIN4 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.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.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 →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.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 →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Garbage collection: why an object is alive when nothing refers to itIn the standard CPython build an object dies the moment its last reference disappears — no collector is needed for that. The collector is needed for cycles, and only for them. And an object that “will not die” may be held not by a garbage cycle but by a reference from a live object that you cannot see in the code: an exception's traceback, a cache key, a method in a subscriber list.garbage-collectionreference-countinggcweakrefmemory-leaks25 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 by repetition. 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.14Modules and imports: a module runs once, and from copies a referenceQuestions about imports seem like housekeeping until you are asked what exactly runs on import. A run answers: the module body ran once for a thousand and three imports — and again once its entry was deleted from sys.modules; after from settings import DEBUG, the assignment settings.DEBUG = True did not show the new value; a circular import failed on from and worked once the access to the module moved inside a function; and a file of your own named random.py next to the script shadowed the standard module.import-systemmodulespackagessys-modulescircular-import35 MIN4 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14MRO and multiple inheritance: C3, the diamond, and where super() really goesThe method lookup order is not “left to right, depth first” but the C3 linearization, and it follows from a single merge rule. The same rule gives both the diamond, where the common ancestor goes to the tail, and the TypeError with which Python refuses to create a class. And super() goes not to the parent but to the next class in the instance's MRO — so the same method calls different neighbours in different hierarchies.mrosupermultiple-inheritancec3-linearizationcooperative-inheritance25 MIN4 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 →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Strings and formatting: immutability, slices, f-strings and the cost of concatenationQuestions about strings check whether you understand that a string is immutable and what follows from that. A run answers: s[0] = 'H' gives a TypeError; after t = s; s += '!' the name t still sees the old string; slice bounds are clipped to the string's length — 'Python'[10:20] is empty, 'Python'[:100] is whole, while an index beyond it is an IndexError; f'{2.5:.0f}' prints 2, because rounding goes to even; a six-letter Cyrillic word is six code points and twelve bytes; and CPython's fast path for s += x does not always apply: an extra reference, a computed hash, a str subclass, a non-ASCII character and — on 3.13 and 3.14 — a second statement on the same line of code switched it off. join is the reliable way to build a string.strings-formattingf-stringsstr-bytes35 MIN6 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Threads and processes: what to choose for computation, and what for waiting"Threads or processes?" is a question answered with a rule and checked by understanding the reason. A run on CPython 3.13.7 (the regular build with the GIL, Linux, 2 cores) answers: four computational tasks in four threads took 0.478 s versus 0.502 sequentially, in four processes 0.262; four waits of 0.2 s in threads took 0.201 s; a thread writes to the shared list, while a process writes to its own copy; a lambda cannot be passed to a process pool; a process pool is created in main() and run as a file; and on 3.14 on Linux processes start with forkserver rather than fork by default.threads-vs-processesthreadingmultiprocessingconcurrent-futures25 MIN6 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Testing: unittest, pytest and mock — what is really being checkedQuestions about tests check not syntax but whether you understand what a test really checks. A run answers: a failed check and an exception in a test are different outcomes; patch('time.time') does not replace the time in a module that did from time import time; a Mock without a spec accepts a call with the wrong number of arguments and a method with a typo in its name; a typo in the check name m.called_once() passes silently on 3.11 and is an error since 3.12; and unittest runs tests in alphabetical order, not in the order they are declared.testingunittestpytestmock40 MIN3 SOURCESRead →IntermediatePublished3.11 · 3.12 · 3.13 · 3.14Environments and dependencies: what venv does, and what pip doesQuestions about virtual environments seem mundane until you are asked how the interpreter knows it is in an environment. A run answers: an environment is a folder with its own bin/python and a pyvenv.cfg file; env/bin/python sets sys.prefix to the environment without any activation; activate is a shell script that puts env/bin first in PATH and sets VIRTUAL_ENV and VIRTUAL_ENV_PROMPT; a package installed into the environment is not visible to the base interpreter; pip freeze prints a snapshot of what is installed, not a lock file; a fresh environment has two packages on 3.11 — pip and setuptools — and one, pip, since 3.12; and since 3.14 the environment's sys.prefix is set by path initialization, so python -S no longer resets it.venv-dependenciesvenvpiprequirements25 MIN7 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 →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-descriptorsemfile25 MIN4 SOURCESRead →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 →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 — and a timeout set on the socket does not cover them.srelinuxdnsgetaddrinforesolver30 MIN4 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.srequeueinglatencypercentilescapacity30 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 →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 →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 →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.

GIL: not “Python can't do threads,” but “a thread waits exactly as long as you asked”articleThe 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.__slots__: what it changes in the object model and what it costs in bytesarticleThe 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.Python dictionaries from the inside: the CPython hash tablearticleHashing, 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.Generics in Go: eleven types, six code bodies, and a method still reached through a dictionaryarticleThe language first: a constraint defines a type set, a type set defines the operations, and no interface can preserve the relation between an input type and an output type. Then the compiler: Go emits a version not per type but per shape — eleven types gave six code bodies, three different pointers gave one. And only then the measurement: a generic over a constraint with methods gives no direct call and trails fourfold, but four fifths of that four is lost inlining rather than indirection, and here that is measured rather than supposed.

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.

30 nodes · 30 edges

No connections: maps

Edges as a list · 29