◈ python mapVol 4 · Ch 50/62
Volume 4 Python, from the metal up · chapter 50

50The linked list — data joined by pointers

In Chapter 49 we built the array: one contiguous block, where order is nothing but adjacency. Element 5 sits right after element 4, and "next" means a few bytes further along. The linked list is its exact opposite, and I want to take the trade slowly, because it's the cleanest example in the whole volume of buying one power by selling another. Here's the plan. We scatter each element into its own little object on the heap, a node, and we hand every node a pointer to where the next one lives. Order stops being about addresses and starts being about arrows. The whole way through we keep asking the one question that decides everything: if the sequence lives in the pointers instead of the layout, what does that make almost free, and what does it make ruinously expensive? By the end you'll know why splicing into a linked list is O(1) when you already hold the spot, and why finding that spot is O(n). You'll see why a scattered list crawls even though it's "the same" O(n) as an array — and when that trade is exactly the one you want.

iolinked · chapter 50 — the checkpoints6 steps
$ sections covered in The linked list — data joined by pointers
01Order without neighbours
02The pointer tax
03Insertion is two pointers — given the node
04…but finding it is a walk
05Same O(n), wildly different speed
06Two-way streets, and where this really lives

01Order without neighbours

Let's start with how an array keeps order, because the linked list is defined entirely by rejecting it. An array keeps order by adjacency. Element 5 sits right after element 4 in one block, so "next" just means "8 bytes further along" (Volume 1, chapter 8). A linked list throws that rule out. It scatters its elements anywhere the heap has room. Each element carries a note saying where the next one is. That note is a reference, the same 8-byte machine pointer that names use in Volume 1. The container remembers just one thing: a head reference to the first node. Follow the notes and the sequence unspools, even though no two nodes need sit anywhere near each other in memory.

Read that slowly, because it's the whole idea and everything else in the chapter falls straight out of it: logical order lives in the pointers, not in the addresses. Two nodes that are "adjacent" in your playlist can sit on opposite ends of RAM. That one decision buys the linked list its superpower. You can rearrange the sequence without moving a single byte of data. It also saddles the list with its curse. You can't jump to the middle, because there's no address to compute. Let's watch it happen in memory first.

★ YOU ALREADY RUN THIS · the-treasure-huntevery clue knows one thing only: where the next clue is
You have run a treasure hunt for a seven-year-old. The first clue is taped to the fridge and it says exactly one thing: look under the blue chair. Under the chair is the next clue, which names the next place, and on it goes to the prize in the shoe cupboard. Nobody in that game can skip to clue seven. There is no map and no list of hiding places — the order lives entirely in the clues, handed over one at a time. But that is also why you could change the hunt thirty seconds before the kids arrived: to add a stop, you rewrote one clue and taped it to a new card. Nothing else in the house had to move.
reach clue 7 — walk clues 1 through 6node_at(head, 6) · k hops · O(n)
the clues are hidden all over the housenodes scattered on the heap — no base + i×stride to compute
adding a stop mid-hunt — rewrite ONE cluex.nxt = a.nxt; a.nxt = x · O(1)
every clue costs a card and a strip of tapea 48 B node against an 8 B slot — the pointer tax
you are already standing at the blue chairhold the handle, and the splice is free
pin it: the array computes where THU is; the chain can only ever be told what comes next — so reaching clue 7 costs seven steps, and changing the hunt costs one card.

Before we count bytes, hold one node in your hand and picture a playlist of three songs: Levels → Wake Me Up → Titanium. Each node is a tiny two-cell box. The first cell holds a reference to its value, the song itself, and the second holds a reference to the next node, the arrow. The Levels node doesn't contain Wake Me Up inside it — it just knows the address where that node lives. The figure below lays the three boxes out at unrelated addresses, and the only thing stitching them into an order is those arrows. Trace them once with your finger before we start pricing them.

head a reference node @ 0x…c0 value next node @ 0x…14 value next node @ 0x…9c value next None ⏚ "Levels" "Wake Me Up" "Titanium" Blue = a reference to the next node · amber = a reference to the value · the arrows, not the addresses, hold the order.
Fig — A singly linked list of three songs. Each node is a two-cell box: a reference to its value and a reference to the next node. The nodes sit at unrelated addresses (…c0, …14, …9c); the sequence exists only because the blue arrows chain them, ending at None.

If order is just arrows, rearranging the playlist should be almost free — no block to shift. Let's cash that in. But first: what does one node actually cost? →

02The pointer tax

Nothing is free, and the linked list's bill comes due in bytes. In an array (a Python list), each element is stored as one bare 8-byte reference, packed shoulder-to-shoulder in a shared block. The object header is paid once for the whole list. A linked list can't do that. Every element is its own heap object, so every element pays the full Python object tax again. I measured a minimal node on CPython 3.12. It's a class with __slots__, so it carries no per-instance dictionary:

cost.pypython
import sys

class Node:
    __slots__ = ('value', 'next')      # no per-instance __dict__
    def __init__(self, value):
        self.value = value             # a reference to the song
        self.next  = None              # a reference to the next node (or None)

a = Node("Levels"); b = Node("Wake Me Up"); c = Node("Titanium")
a.next = b; b.next = c                  # a -> b -> c -> None
print(sys.getsizeof(a))                 # 48   bytes per node

48 bytes. The node holds only two useful references, which is 16 bytes of actual pointers. Those sit wrapped in 32 bytes of Python machinery: the reference count, the type pointer, and the garbage-collector bookkeeping every tracked object carries (Volume 1, chapter 4). So to store one element's worth of sequence, an array spends 8 bytes and a linked list spends 48. That's a 6× memory tax, 40 extra bytes on every single node. That surcharge is what buys the flexible arrows, and it is never free.

Break that 32-byte header open and the tax is easy to audit. Eight bytes hold the reference count, eight more hold the type pointer that says "I'm a Node", and the last sixteen are garbage-collector bookkeeping: 8 + 8 + 16 = 32, and the two useful pointers then push that to 48. And here's the quiet twist. Those 16 GC bytes are themselves two 8-byte pointers, threading this node onto a hidden doubly linked list that the collector keeps — a linked list riding inside every node of your linked list. Hold that thought; we cash it in at the end of the chapter.

Let's make that surcharge concrete at scale, because 40 bytes sounds tiny until you multiply. Say you store a million songs. The array packs them as a million bare references: 1,000,000 × 8 bytes = 8 MB, one clean block. The linked list pays the full node price on each one: 1,000,000 × 48 bytes = 48 MB. Same million songs, and the linked version has eaten an extra 40 MB of RAM to hold nothing but reference counts, type pointers, and arrows. The tax didn't stay small. It grew in lockstep with your data, exactly as "per element" promised.

The tax is per element, so it never stays small. It scales with your data. Drive the count up and watch the same numbers cost three wildly different amounts of memory, decided by nothing but how you wrap them. Watch, too, where each layout spills out of cache.

InteractiveStore a million numbers — pay the pointer tax
the same N numbers, stored three ways — what does the sequence itself cost? N = 1,000,000 numbers values are shared by all three — this is the container's own plumbing array 8 B / elem 8.0 MB · fits L3 ✓ slotted node 48 B / elem 48 MB · spills ✗ un-slotted node 344 B / elem · no __slots__ 344 MB · spills ✗ UN-SLOTTED LIST · PLUMBING FOR THE SAME NUMBERS 344 MB = 43× the array's 8.0 MB — for identical data At 1,000,000 numbers the array's links still fit a 32 MB L3 cache; the un-slotted nodes miss it by 11×.
1,000,000
Slide from a thousand to ten million. The array keeps its sequence in 8 bytes/element; a slotted node pays 48, an un-slotted node 344 — a 43× blow-up for the identical numbers, almost all of it Python bookkeeping the CPU never reads. Notice who still fits a 32 MB cache and who doesn't: layout alone decides.
one linked-list node — 48 bytes object header · 32 B value ref8 B next ref8 B 40 B tax one array slot — 8 bytes value ref8 B …header paid once for the whole block
Fig — The pointer tax. A node spends 48 bytes to carry the same 8-byte reference an array slot holds bare — 40 bytes of per-element overhead (a 32-byte header plus the extra next pointer). Multiply by a million nodes and it's real memory.
THE STDLIB TOOLBELT · the dataclass nodetwo fields and one arrow — the whole linked list, in five lines you would actually type
from dataclasses import dataclass @dataclass(slots=True) # slots=True -> 48 B/node, no per-instance __dict__ class Node: val: int nxt: "Node | None" = None # quoted: the name Node isn't bound yet head = Node(value, Node(value)) # build it inside-out, tail first while cur is not None: # the only route through a chain cur = cur.nxt
@dataclassWrites __init__, __repr__ and __eq__ for you — exactly the boilerplate a node needs, and none of the typing. This is the reason hand-rolling a node in modern Python is five lines, not fifteen.
slots=TruePython 3.10+. Keeps the node at the 48 bytes section 02 metered. Leave it off and the same node costs 344 — measured on this machine — because it grows a __dict__. Not optional.
"Node | None"A quoted forward annotation. Inside the class body the name Node does not exist yet, so the string is the only legal way to say “another one of me, or the end”.
= NoneThe default that makes Node(3) a legal tail. None is the chain's terminator — the thing the walk tests for, and the thing the last arrow points at.
headThe container's one handle. Everything else is reached through it, and the moment you lose it every node behind it becomes unreachable and is collected.
field(default_factory=list)The escape hatch if a field ever needs a mutable default. A bare = [] would be one list shared by every node you ever make — @dataclass refuses it outright, and rightly.
INPUTimport sys
from dataclasses import dataclass

@dataclass(slots=True)
class Node:
    val: int
    nxt: "Node | None" = None

head = Node(1, Node(2, Node(3)))     # build it inside-out

cur, total, hops = head, 0, 0
while cur is not None:               # the ONLY way through a chain
    total += cur.val
    hops += 1
    cur = cur.nxt
print("summed", total, "in", hops, "hops")
print("one node  :", sys.getsizeof(head), "bytes  (slots=True)")
print("repr(head):", repr(head))
OUTPUTsummed 6 in 3 hops
one node  : 48 bytes  (slots=True)
repr(head): Node(val=1, nxt=Node(val=2, nxt=Node(val=3, nxt=None)))
TRIPWIRES
  • That free __repr__ is recursive. It is charming on three nodes, as above, and on a 2,000-node chain repr(head) raises RecursionError: maximum recursion depth exceeded — run here to be sure. Print [n.val for n in walk(head)], never the head itself.
  • The generated __eq__ recurses too. Two structurally equal 2,000-node chains and a == b raises the same RecursionError. When you mean “the same node”, write a is b.
  • End the walk with cur is None, never cur == None. == calls that generated __eq__ on every single hop: the identical 2,000-node walk measured 21.0 µs with is not None against 224.7 µs with != None — 11×, for one wrong operator.
Why 32 bytes of header, when Volume 1 said ~28?
Volume 1's "~28-byte header" was measured on a small int (sys.getsizeof(5) is 28) — that figure folds in the int's own digit storage. Our node has no payload of its own beyond two references, so it shows the header alone: 16 bytes of core object (the reference count and the type pointer) plus a 16-byte garbage-collector head that every container-like object drags along so the cycle collector can find it. That's 32 bytes of pure Python bookkeeping before a single useful pointer. Both numbers are honest; they're measuring different objects.
The 344-byte node — a tax you can accidentally 7×
Drop the __slots__ line and every node grows a __dict__ to hold its attributes. I measured it: the same node balloons from 48 bytes to 344sys.getsizeof(fn) + sys.getsizeof(fn.__dict__). Seven times heavier, for identical data. If you ever hand-roll a node-based structure in Python, __slots__ is not optional; it's the difference between a lean list and one that evicts your working set from cache. Most of what's "slow about linked lists in Python" is really the un-slotted node.

So a node is heavy but its arrows are flexible. Now spend that flexibility — insert a song mid-playlist and count what actually moves. →

03Insertion is two pointers — given the node

Here's the linked list's marquee trick. To insert a new node X between two existing nodes A and B, you do exactly two things: point X.next at B, then point A.next at X. Two reference writes. Nothing else in the entire structure is read or moved, not the million nodes before A, not the million after B. Compare an array, where inserting at position i must shift every element from i onward one slot down to open a gap (Volume 1, chapter 8). That's O(n) copies. The linked list splice is O(1), and here's the beautiful part. It stays O(1) no matter where in the sequence you splice, because the cost doesn't depend on position at all. Order is arrows, and you're just re-drawing two of them.

One quiet detail decides whether that splice works or corrupts the list: the order of the two writes. You must set X.next to B first, while A.next still points at B and can tell you where B is. Then, and only then, set A.next to X. Do it the other way round and you overwrite A.next before you've read it. Now B's address is gone, and the whole tail of the list after A has fallen off into the void. Same two writes, opposite outcome. When you re-wire pointers, the sequence you read and overwrite them in is part of the algorithm, not a detail.

Let's put addresses on it so the ordering isn't abstract. Say node A lives at address 200, B at 350, and the new node X at 500. Right now A.next holds 350, pointing straight at B. First we set X.next = 350, so X now knows where B is. Only then do we set A.next = 500, so A points at X instead. Read the chain from A and you get 200 → 500 → 350, exactly A → X → B. Reverse the two writes and A.next becomes 500 before anyone saved 350 — so B's address is lost, and everything past it leaks away.

A B C old A.next (discarded) B.next — untouched X (new) ① A.next = X ② X.next = B Two writes. Zero elements moved. And it's the same two writes whether the list holds 3 nodes or 3 million.
Fig — A splice rewires exactly two pointers: A.next = X and X.next = B. The old A→B arrow is simply dropped. Constant work, independent of list length or position.

The widget below makes it literal. Slide to pick where the new node goes, and watch what each side has to do. The linked list always writes exactly two pointers, no matter where you drop the node. The array beside it must shift a different number of elements depending on where you cut. And the further left you insert, the more it copies.

InteractiveSplice a node in — watch exactly two pointers rewire
2
The linked list writes 2 pointers at every gap — O(1). The array's shift count changes with position — O(n). Same edit, different cost.
↺ The thing people get backwards
"Insertion in a linked list is O(1)" is the most over-quoted half-truth in data structures. It's only O(1) once you already hold the node to splice at — handed to you by an iterator, a hash-map entry, or an LRU handle. If you have to find that spot first — "insert after the node whose value is Titanium", or "insert at index 500" — you pay O(n) to walk there, and the walk dominates. So mid-list insertion, start to finish, is O(n) for a linked list too. The linked list doesn't make insertion cheap; it makes it cheap given a reference to the location. The whole game is arranging for something else to already own that reference.

Which raises the obvious question: if I only have an index — "give me the 500th song" — how does a linked list find it? →

04…but finding it is a walk

Ask an array for element 500 and it does one multiplication: base + 500 × 8. Jump straight there, done. That's O(1), the reward for living in one block (Volume 1, chapter 8). A linked list can offer nothing like it. There is no base address and no arithmetic, because the nodes are scattered and only the arrows know the order. To reach index k you must start at head and follow next k times, node by node. Reaching the 500th node means visiting the other 500 first. That's O(n), baked into the layout, the price of trading adjacency for arrows.

I measured the gap on a million-node list. Walking to the middle took about 9 milliseconds; the array's lst[k] took about 0.1 microseconds — the O(n) walk versus the O(1) address computation. Most of those 9 ms is simply the half-million pointer hops the walk is forced to make; the array never hops at all. (Wall-clock numbers vary by machine; the O(n)-vs-O(1) shape does not.)

Put a clock on a single hop and the O(n) gets vivid. Nine milliseconds is 9,000,000 nanoseconds, spread across the half-million hops the walk makes: 9,000,000 / 500,000 ≈ 18 nanoseconds per hop. That's the cost of one link followed — read the node, grab its next, move on. The array pays this zero times: it computes an address and lands in a single shot. So the linked list's bill for reaching the middle isn't one slow step, it's half a million ordinary steps with nothing to skip them. There is no arithmetic shortcut when order lives in the arrows.

Here's the arithmetic behind that walk. Reaching index k costs exactly k hops, so index 0 is free and the last index costs the whole list. Average that over every position and a random lookup runs about n/2 hops. On a million-node list that's 500,000 pointer hops for one index, every single time. An array answers any of them with one multiplication. That distance between 500,000 and 1 is what O(n) versus O(1) means in hardware, not on paper.

linked list: walk from head, hop by hop 0 1 2 k hop · hop · hop … k times → O(n) array: one multiply-and-add base + k×8 → O(1)
Fig — The mirror image of insertion. The array indexes in one arithmetic step; the linked list must walk k hops from the head because only the arrows encode position. Great at splicing, slow at seeking.

Drive the walk yourself. Slide the target index and watch the current pointer crawl out from the head, one hop per node. Count them as they go, because the hop count is the index — that equality is the whole cost of a linked-list seek.

InteractiveWalk to index k — count the hops
0
Reaching index k costs k hops from the head — an array would reach it in zero. This is why random access on a linked list is O(n).

Myth

"Linked lists beat arrays because you never shift elements — so use them when you insert a lot."

Reality

Only if you already hold the node. If you insert "after the item I'm looking at while iterating", the linked list wins. If you insert "at index i" or "after the item with value v", you pay O(n) to find the spot. And then the array's fast index and cache locality usually make it the better choice anyway.

There's a sharper version of that trap, and it catches people constantly: even appending to the end of a plain singly linked list is O(n). Think it through — the container holds only head. To reach the last node so you can hang a new one off it, you have to walk every link from the front. The splice itself is still two writes, but getting to the tail is a full traversal. The fix is to cache a second reference, a tail pointer, updated on every append. Then the end is one hop away and the append is genuinely O(1). It's the same lesson underneath: a linked list is only fast where you already hold the handle, so real implementations keep handles to the spots they touch.

So when do you genuinely already hold the node? More often than the myth suggests. Think of a music player showing your queue, where you drag Titanium up two spots. The interface is already pointing at that exact node, so the move is a splice, not a search. The same is true inside an LRU cache, where every entry is a node you reached through a hash lookup, and you re-order it on each hit. In both cases the find is free, because something else already handed you the handle. That is the shape of the problem where a linked list quietly wins.

One more place the handle comes for free: a running iterator. When you loop through a linked list and decide to insert or drop as you go, you're already standing on the node. The loop variable is the handle. That's why merging two sorted linked lists splices in O(1) per step: the walk that visits each node also hands you the exact spot to re-wire. The find isn't a separate cost. It's the traversal you were doing anyway.

NOW TURN THE ARROWS AROUNDreverse the chain with three names — then prove that not one value moved
The drill. Write reverse(head) for the dataclass Node using exactly three names: prev, cur and nxt. Walk the chain once, turn each arrow to point at the node behind it, and return the new head.

The rule that makes it work is section 03's write-order lesson, running backwards: save cur.nxt into nxt before you overwrite cur.nxt, or the entire tail falls off into the void on the very first turn of the loop.

Then prove the claim. This chapter says reversal moves no data, only arrows. Don't take it on trust — collect id(n) for every node walking the chain before, and again after. If it is true, the after-list is the before-list backwards, and the count of nodes created during the reversal is exactly zero.
show the solution
from dataclasses import dataclass

@dataclass(slots=True)
class Node:
    val: str
    nxt: "Node | None" = None

def walk(head):                       # yields the nodes themselves, in order
    cur = head
    while cur is not None:
        yield cur
        cur = cur.nxt

def reverse(head):
    prev, cur = None, head
    while cur is not None:
        nxt = cur.nxt        # 1. remember the way forward, or you lose the tail
        cur.nxt = prev       # 2. turn this arrow around
        prev, cur = cur, nxt # 3. shuffle both walkers one node along
    return prev              # cur fell off the end; prev is the new head

head = Node("Levels", Node("Wake Me Up", Node("Titanium")))
before_ids = [id(n) for n in walk(head)]
print("before:", [n.val for n in walk(head)])

head = reverse(head)
after_ids = [id(n) for n in walk(head)]
print("after :", [n.val for n in walk(head)])

print("same node objects, reversed order:", after_ids == before_ids[::-1])
print("nodes created during reverse     :", len(set(after_ids) - set(before_ids)))

# before: ['Levels', 'Wake Me Up', 'Titanium']
# after : ['Titanium', 'Wake Me Up', 'Levels']
# same node objects, reversed order: True
# nodes created during reverse     : 0

# READ THE LAST TWO LINES. Every id() came back, in the opposite order, and
# nothing new was allocated. The three song strings never moved, the three
# nodes never moved -- three assignments to .nxt reversed the whole sequence.
# An array reversal has to write every slot. This wrote the arrows and left
# the data exactly where the allocator first put it.
# The one line that carries the whole algorithm is nxt = cur.nxt. Delete it
# and cur.nxt = prev overwrites the only pointer to the rest of the list,
# so the loop ends after one node and the tail is unreachable forever.

O(n) to walk, O(n) to scan — the same big-O as an array. So a full pass over a linked list and a full pass over an array should take the same time, right? Time them and prepare for a shock. →

05Same O(n), wildly different speed

This is the section that separates people who know Big-O from people who know the machine. Summing a linked list and summing an array are both O(n): n additions, one per element. Big-O says they should scale identically, and they do. But wall-clock time is a different story, and the reason is the memory hierarchy from Volume 3. The CPU never fetches one value from RAM. It fetches a whole cache line, 64 bytes, eight references' worth, and keeps it in fast on-chip memory. An array is one contiguous block, so a single fetch pulls in the next eight elements for free. The scan streams: fetch a line, use eight, fetch the next. This is called spatial locality, and hardware is built to reward it.

A linked list has none of it. Its nodes are scattered wherever the allocator found room, so following next lands you at an unrelated address every time. Each hop is a fresh cache miss: the CPU stalls, waits a hundred-odd cycles for RAM, and pulls a 64-byte line only to use 8 bytes of it. Then it hops somewhere else and pays the whole price over again. This is pointer-chasing, and it's the linked list's hidden tax. I measured a two-million-element sum three ways:

cache.pypython
array sum (contiguous)                     49 ms   (24 ns/elem)
linked list sum (nodes in alloc order)     55 ms   (28 ns/elem)
linked list sum (nodes shuffled in RAM)   407 ms  (204 ns/elem)

Same n, same additions, same O(n). Yet the scattered linked list ran roughly 8× slower than the array on that run, and about 6× on a second run. It's a cache effect, so the exact ratio drifts with the machine and memory state. But it's always a large constant, never 1×. Look closely at the middle row. A linked list whose nodes happen to be allocated in order is nearly as fast as the array, because consecutive nodes land near each other and the scan stays cache-friendly. It's the scattering that kills it, not the linking. Two structures, one complexity class, an order-of-magnitude difference in the only number the user feels.

Here's the same story counted in fetches, so the 8× stops feeling like magic. A cache line is 64 bytes, which is 64 / 8 = 8 references. To sum two million array elements, the CPU streams the block in 2,000,000 / 8 = 250,000 line fetches, and every fetch delivers eight useful values. The scattered linked list can't share a line between nodes, so it pays up to 2,000,000 fetches, one per hop, and throws away 56 of every 64 bytes it drags in. Roughly eight times the memory traffic for the identical arithmetic. That ratio is the 8× you measured, and it was hiding in the geometry all along.

array: one 64-byte line = 8 useful refs r r r r r r r r 1 miss → 8 used ✓ scattered list: one line = one node, 7/8 wasted nodewasted nodewasted nodewasted 1 miss per hop → the CPU stalls, over and over
Fig — Why the same O(n) diverges. The array's contiguous block turns one memory fetch into eight useful elements; the scattered list burns a whole cache-line fetch per node and stalls on every hop. Cache locality is the difference between 24 ns and 200 ns per element.
Big-O hides the constant that ate your afternoon
Two algorithms with the same Big-O can differ by 10× in practice, and cache locality is the usual culprit. This is exactly why Python's own list and deque — both built on contiguous blocks of references — beat a hand-rolled node-per-item linked list for almost every real workload, even the ones textbooks say "favour" linked lists. When you profile and two "equally fast" options aren't, suspect the memory layout before you suspect the algorithm.

If it's heavier and slower to scan, why does the linked idea sit at the heart of some of the most-used software on your machine? Because there's a job only it can do. →

06Two-way streets, and where this really lives

Add one more pointer to each node, a prev reference back to the previous node, and you get a doubly linked list. Now you can walk either direction. More importantly, you can delete a node in place when you're standing on it. Reach both neighbours through prev and next, wire them to each other, and the node is gone in O(1). There's no need to have walked from the head to find the one before it. My measurement puts a doubly linked node at 56 bytes versus the singly node's 48: one extra 8-byte pointer, the fee for two-way travel. Link the tail's next back to the head and it becomes circular, no beginning, no end, useful for round-robins.

Picture three nodes A ⇄ B ⇄ C and say you want B gone. Standing on B, you read its two neighbours: B.prev is A, B.next is C. Two writes finish the job — set A.next = C, then C.prev = A. Now A and C point straight at each other, B is unreachable, and the reference count that kept it alive falls to zero. No walk from the head, no shifting of neighbours: O(1), decided entirely by the two pointers you were already holding.

Watch the reference count do the cleanup for you — this is where Volume 1's memory model pays off. While B sat in the chain, two live pointers from inside the list named it: A.next and C.prev. So its structural refcount was 2. The moment you rewire both — A.next = C, then C.prev = A — those two references vanish, and the count drops 2 → 1 → 0. Once your own local handle on B is gone too, CPython reclaims its 56 bytes on the spot (Volume 1, chapter 4); you never call anything like free. Unlinking a node and freeing its memory are the same event, and both are O(1).

Where does a two-way or circular list actually earn its extra pointer? Wherever you need to step backward or loop forever. A media player's previous-track button wants prev, so pressing it is one hop, not a walk from the start. An operating system's run queue of ready processes is often circular. The scheduler hands each one a time slice, then follows next to the following process, and the tail loops back to the head so the rotation never ends. No beginning, no end, no special case for "wrap around". The extra 8 bytes per node buys exactly that freedom of movement.

prev · val · next"Levels" prev · val · next"Wake Me Up" prev · val · next"Titanium" next → ← prev With prev and next, deleting the middle node is O(1) from the node itself — wire its two neighbours together.
Fig — A doubly linked node carries prev and next (56 bytes vs 48). Two-way arrows let you delete a node you're standing on in O(1), without ever walking from the head.

You almost never type class Node in Python. The built-in list and deque (Chapter 53) cover the common needs faster, thanks to their block layout. The linked list's real importance is that the idea is everywhere underneath, doing the one job it owns. That job is rearranging a sequence, or splicing and unsplicing elements, in O(1) when you hold a handle to the spot. It does this with no reallocation, and with node identities that stay valid while the structure changes around them.

Here's the one that surprises people: CPython itself runs on linked lists. Every container object the garbage collector tracks is threaded onto a doubly linked list, and that threading lives inside the 32-byte header we metered back in section 02. The collector walks that list to hunt unreachable cycles, and it splices objects on and off it in O(1) as they're created and freed. So the pointer tax from section 02 wasn't pure overhead — part of it was buying exactly this. You've been leaning on a linked list on every line of Python you've ever run, one level down.

Where you meet this — every day, mostly hidden
An LRU cache — the thing that decides what your OS, your browser, and your database keep in fast memory — is a hash map paired with a doubly linked list: a cache hit splices its node to the front in O(1); the least-recently-used item falls off the back. Python's own functools.lru_cache and OrderedDict are built exactly this way. The Linux kernel threads a circular doubly linked list (list_head) through nearly everything — process tables, scheduler run-queues. A memory allocator tracks free blocks as a linked free list. A blockchain is a backward linked list — each block points to the previous one's hash — and so is a chain of git commits, each pointing at its parent. Even the classic FAT filesystem stored each file as a linked list of disk clusters. You used an LRU cache to load this page.
The one-line tell for reaching for the linked idea
Being good at data structures is mostly matching an access pattern to a layout. Reach for a linked structure when your pattern is: I hold a reference to a position, and I splice or unsplice there repeatedly, and I rarely need to jump to element i. LRU eviction, an event list you thread and re-thread, a free list, adjacency lists in a graph (Chapter 60), the collision chains inside a hash table (Chapter 54) — all that shape. If instead your pattern is "give me element i" or "scan it fast", the contiguous array wins on both counts, and you should feel the pull toward a list or deque.
The deeper cut
The linked list is one of computing's oldest data structures — Newell, Shaw, and Simon built it into IPL around 1955–56, the list-processing language that seeded LISP. The insight was radical for the era: represent a structure's order with explicit links rather than physical adjacency, decoupling logical shape from memory layout. That decoupling is precisely what lets a tree (Chapter 56) or a graph (Chapter 60) exist at all — they're just nodes with several pointers each instead of one. A subtlety worth knowing: real systems often avoid the 48-byte-per-item tax with an intrusive list, where the next/prev links live inside the data object rather than in a separate wrapper node (the kernel's list_head is embedded in each struct). And an unrolled linked list — several elements packed contiguously per node — is the halfway house the deque takes to the extreme in Chapter 53, trading some pointer flexibility back for the cache locality you just watched matter so much.

One more thing only a linked structure gives cheaply: reversal. Flip every node's arrow so it points at the one behind it, and the whole sequence runs backward with no data copied — just pointers turned around. In an array you'd have to move every element to a new slot. Here you touch nothing but the links. Watch it move only pointers:

arp.pypython
def reverse(head):
    prev = None
    cur  = head
    while cur is not None:
        nxt = cur.next     # remember where we were going
        cur.next = prev    # flip this node's arrow backward
        prev = cur         # advance the two walkers
        cur = nxt
    return prev            # prev is the new head

# a -> b -> c  becomes  c -> b -> a
# before: ['Levels', 'Wake Me Up', 'Titanium']
# after : ['Titanium', 'Wake Me Up', 'Levels']

Line by line: prev trails behind and cur leads. Each turn of the loop stashes the forward link in nxt, or we'd lose the rest of the list the instant we overwrite it. Then it flips cur.next to point backward at prev, and shuffles both walkers one step forward. When cur runs off the end, prev is left holding the last node, the new head. I ran it on Levels → Wake Me Up → Titanium and got Titanium → Wake Me Up → Levels. Not a single value object moved in memory. Only the arrows turned. That is the linked list's whole personality in eight lines: cheap to re-wire, and it never touches the data.

Wait —
if splicing at a held node is the linked list's superpower, what if we deliberately only ever touch one end? Restrict it hard enough and you get a structure so useful it runs the call stack, every Undo button, and the Back button in your browser…

The next chapter does exactly that. Take a list, forbid every operation except push and pop at one end, and you get the stack — the simplest container there is, and the one quietly running your program's function calls right now. →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 50, in working code

Twelve tiny linked lists you can actually run — we build the node from raw primitives, splice and seek and reverse it, then watch the very same O(n) cost a fortune in cache, and every number below is the machine's own, printed live.

Order lives in the arrows, not the addresses
Build the node from primitives, then walk it — the sequence exists only because each node points to the next, so we can reorder the whole playlist without moving a single value.
Splice is cheap, seek is dear
The linked list's personality in three runs: inserting given the node is a flat two writes, but reaching index k, or the tail, is a hop-by-hop walk — O(1) one way, O(n) the other.
The hidden bills — bytes and cache
Same data, two layouts. A node pays a 6x memory tax for its flexible arrows, and a scattered scan pays it again in cache misses — the constant Big-O quietly throws away.
Two-way streets, and where this really lives
Add a prev pointer and close the ring, and the linked idea shows its real face — reverse in place, delete where you stand, cycle forever, and run the LRU cache that loaded this page.
end of chapter 50 · six sections
iolinked
designed & developed by Ajai Raj
Python, from the metal up · a product of iolinked