Deep Engineering
Advanced·Published·3.12 · 3.13 · 3.14·35 MIN

Lists from the inside: growth, shrinking, and the price of the front

A list has two lengths, and the second one has no public name in Python. Everything else follows from it: why lists that look identical take different amounts of memory, why append is cheap while insert(0) is quadratic, and why from 3.13 onwards del L[0] and L.pop(0) leave the object in different states — at the same length of a hundred elements the list takes 8056 bytes instead of 1528.

Full technical treatment

A list has two lengths, and this article is about the second one.

Everybody knows the first: len(L), the number of elements. The second has no public name anywhere in Python — the number of pointer slots already allocated. The usual words are length and capacity, and nearly everything a list does is explained by the second number rather than the first.

As long as capacity stays invisible, none of this can be explained. Why two lists of the same ten numbers take different amounts of memory. Why append is cheap on average when growing the array sometimes means moving all of it. Why, after nine hundred removals from a thousand-element list, it may still be occupying room for a thousand — and exactly when that happens.

The article works upward: first the two numbers and how they relate, then what deletion does, and only then time. Time comes last not because it matters least but because without the first two parts it reads as a pile of unconnected facts.

Part I. Length and capacity

Why a list carries spare room

A list in CPython is three levels, and they are worth separating up front: the PyListObject itself, a separate contiguous array of pointers, and the objects themselves, which live anywhere in memory. Two things follow from that: what getsizeof counts (the header plus the array, not the objects), and why removing from the front shifts pointers rather than elements.

An array can sometimes be extended in place: realloc grows the allocation without moving it when there is free space past its end. What cannot be done is to count on that.

The way to check is the address of the array itself — the ob_item field before and after. Capacity changes not by one but in jumps (the jumps themselves are covered below), and at each of them the run compares two addresses: the same one means nothing was copied. On 3.13.7 with the GIL enabled (on the free-threaded builds this check is not made — ctypes reads the field by offset, and the object layout there is different) — bench/lists/growth.py, block 6:

AppendsCapacity jumpsArray stayed putArray moved
600241311
20000534112

Both outcomes occur — and that is all that follows. The proportion does not reproduce: on 3.12.3 and 3.14.7 the same run gives twelve against twelve, and it drifts between runs as well, because it depends on the state of the allocator and the history of the process. When there is no free space past the end, the allocation moves along with all of its contents, and which time that will be is not knowable in advance.

So the point is not that growing in place is impossible — it is how many times you pay for it. Without spare room the capacity would change on every append; with it, it changes twenty-four times over the first six hundred, and not every one of those costs a copy.

The way out is to allocate with room to spare. The comment above the function that does this states both the reason and the result:

This over-allocates proportional to the list size, making room for additional growth. The over-allocation is mild, but is enough to give linear-time amortized behavior over a long sequence of appends() in the presence of a poorly-performing system realloc().

"Amortized" is not a hedge here: an individual append that lands on a boundary is expensive — it copies the whole array. The boundaries are just spaced so that over a long run of additions the cost flattens into a constant.

That spare room is the second number. Neither len nor any list method shows it. The size of the object does, and here is why.

Capacity is read from getsizeof, and that is not a workaround

CPython's list.__sizeof__ counts by allocated slots, not by length:

C
list___sizeof___impl(PyListObject *self)
{
    size_t res = _PyObject_SIZE(Py_TYPE(self));
    Py_ssize_t allocated = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->allocated);
    res += (size_t)allocated * sizeof(void*);
    return PyLong_FromSize_t(res);
}

That gives the way capacity is read in every measurement in this article:

PYTHON
def capacity(seq):
    return (sys.getsizeof(seq) - sys.getsizeof([])) // struct.calcsize("P")

The divisor is the pointer size rather than an eight: on a 64-bit build it is eight, but writing eight bakes the word size into the formula.

It needs no ctypes — and the usual way of reading the same field does: it addresses object fields through id(obj). In the run this way stands next to a ctypes read of the same field, and the run COMPARES the two rather than declaring them equal: on builds with the GIL they agreed, while on the free-threaded ones ob_size at the same offset reads as zero — the object layout there is different. That is why every measurement takes capacity from getsizeof.

implementation detail · 3.13

That getsizeof counts by allocated is a property of CPython, not a promise of the language. The function's documentation only says that the memory "directly attributed to the object" is accounted for, and what that covers is up to each type's implementation.

Identical lists, different memory

Five ways to build a ten-element list. The length is ten in all of them; the size is 136 bytes for three and 184 for two. One thing separates them: whether the interpreter knew the length up front.

A literal and [0] * 10 know it from the way they are written: both the number of elements listed and the number of repetitions are settled before the list starts filling. range has __len__ for the same purpose, and the list constructor uses it. Room is taken for exactly ten elements, with none to spare.

A list comprehension and list() of a generator do not: a generator says nothing about its length, and elements arrive one at a time. So the same over-allocation as for append applies, and at ten elements it gives sixteen slots.

Forty-eight bytes per list is nothing. But it is nothing multiplied by the number of lists, and it is worth knowing where lists run into the millions: where a comprehension only walks a range, replacing it with list(range(...)) removes that nothing entirely and changes nothing about the result. Where the comprehension computes something, there is no such replacement — and then the spare is the price of computing, not of carelessness.

The growth formula

The spare is not approximate. There is one formula, and it is in the source:

C
new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;

Read it as: the new length, plus an eighth of it, plus six, rounded down to a multiple of four. The over-allocation is a FRACTION rather than a constant: the longer the list, the more is taken in reserve — which is exactly why the number of reallocations over a long run of appends grows logarithmically rather than linearly.

Next to the formula sits a second branch, for the case where the length jumps by a lot at once, as in extend:

C
if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize))
    new_allocated = ((size_t)newsize + 3) & ~(size_t)3;

Its point is that nothing is over-allocated when the jump is already larger than the over-allocation would be. With append the length grows by one, so this branch never fires — which is why the rest of the article only concerns the first formula.

bench/lists/growth.py checks it at every capacity jump: over the first six hundred appends there are twenty-four of them, and all twenty-four matched on five builds: 3.12.3, 3.13.7, 3.14.7 and the free-threaded 3.13.7t and 3.14.7t — every number, down to the last one. The first eight jumps, as the run prints them:

Length at which it jumpsNew capacity
14
58
916
1724
2532
3340
4152
5364

The same sequence is written out in the comment beside the formula: "The growth pattern is: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, ...". After six hundred appends the length is 600 and the capacity 672 — seventy-two slots of spare.

implementation detail · 3.14

Three different claims live here and must not be mixed up. The first is a practical property: append is cheap on average, and that much is promised by the comment in the source. The second is how CPython achieves it: this particular growth formula, an implementation detail that appears nowhere in the language documentation. The third is an observation: twenty-four changes of capacity over the first six hundred appends, identical on five builds. Only the first is something code may depend on.

Part II. What a list gives back

"A list never releases memory" is wrong, and wrong twice

The claim travels in two shapes, and neither survives a check. One caveat up front: what follows is about CAPACITY, the size of the pointer array. Whether the memory went back to the operating system is a question not for the list but for the memory allocator, which usually keeps the freed block. How much memory the process holds according to the system (its RSS) cannot be predicted from these numbers.

First shape: "capacity only grows". It does not only grow. The condition under which the array is LEFT as it is sits in the same list_resize, and shrinking begins where that condition fails:

C
if (allocated >= newsize && newsize >= (allocated >> 1)) {
    assert(self->ob_item != NULL || newsize == 0);
    Py_SET_SIZE(self, newsize);
    return 0;
}

While the new length is at least half the capacity, the array stays and only the length changes. Once the length drops below half, control falls through and the array is reallocated smaller. The run shows exactly that: a thousand pop() calls from the end produce twelve capacity reductions.

Second shape: "if it releases, it always releases". Also no — and this is where the article's real subject begins.

Two operations with one meaning and different traces

Nine hundred elements are removed from the front of a thousand-element list, in two ways. The length afterwards is the same: one hundred. The memory is not.

On 3.12 both operations leave a capacity of 184 — both shrank the array. On 3.13 and 3.14, L.pop(0) leaves the same 184 while del L[0] leaves 1000: a hundred-element list still occupies room for a thousand, 8056 bytes against 1528.

The same holds from the other end, and this half is the one nobody expects. Each operation below was repeated nine hundred times over a thousand-element list, measured on 3.13.7 with bench/lists/shrink.py:

OperationLength afterCapacity after
L.pop(0)100184
del L[0]1001000
L.pop(len(L)//2)100184
del L[len(L)//2]1001000
L.pop()100184
del L[-1]1001000

L.pop() gives capacity back. del L[-1] does not. Two lines that look equally harmless in review.

Why: two different paths in the source

The cause was not inferred from the numbers but read. Before 3.13, deletion by index did nothing itself and handed the work to the general slice-assignment path:

C
static int
list_ass_item(PyListObject *a, Py_ssize_t i, PyObject *v)
{
    if (!valid_index(i, Py_SIZE(a))) {
        PyErr_SetString(PyExc_IndexError,
                        "list assignment index out of range");
        return -1;
    }
    if (v == NULL)
        return list_ass_slice(a, i, i+1, v);

list_ass_slice shifts the tail and calls list_resize — the one with the shrink condition. That is where the capacity reduction came from.

In 3.13 the function gained a variant that runs under a lock, and it performs the deletion itself:

C
static int
list_ass_item_lock_held(PyListObject *a, Py_ssize_t i, PyObject *v)
{
    ...
    PyObject *tmp = a->ob_item[i];
    if (v == NULL) {
        Py_ssize_t size = Py_SIZE(a);
        for (Py_ssize_t idx = i; idx < size - 1; idx++) {
            FT_ATOMIC_STORE_PTR_RELAXED(a->ob_item[idx], a->ob_item[idx + 1]);
        }
        Py_SET_SIZE(a, size - 1);
    }

list_resize is not called at all here: the length drops in place and the array stays as it was. Neither list_ass_slice nor list_resize appears on this path any more.

The change arrived with the free-threaded build — that is where the atomic stores come from, and the _lock_held suffix in the name: the function expects the list to be locked already by whoever called it. The capacity behaviour is a side effect rather than the goal, and that shows in the fact that "What's New In Python 3.13" does not mention it at all: not under language changes, not under optimizations.

implementation detail · 3.13

All of this is how CPython is built, not a rule of the language. No version promises when a list shrinks; what is promised is only that del L[i] removes the element. Writing code that counts on shrinking after pop is exactly as wrong as counting on its absence after del.

What frees capacity for certain

Since the behaviour of an operation cannot be relied on, it helps to know what works regardless of version. bench/lists/shrink.py tries the options on a list whose capacity stayed at a thousand after nine hundred del calls:

What is done to itLengthCapacity
nothing — the list as del left it1001000
L.copy()100100
list(L)100100
L[:]100100
one append and one pop100116

The first three are the same action: build a new list whose length is known up front. Room is taken for exactly a hundred elements, and the old array is released along with the old list.

The last row is not a recipe but a warning: append followed by pop does change the capacity, yet it brings it not to the length but to whatever the formula gives. One hundred and sixteen instead of a hundred — that is spare again.

There is one practical rule: a list that has shrunk a lot and must live on should be REBUILT. L = L.copy() is not superstition: rebuilding by any of the three answers the same way on all the builds checked, while the deletion operations do not.

Part III. The price of the front

Quadratic behaviour, shown rather than named

"insert(0) is quadratic" is true, but on its own it is just words. The check is doubling: if time grows as the square, doubling the length gives four times the time.

Filling a list through insert(0), measured with bench/lists/ops.py:

LengthTotal, µsns per operationAgainst the row above
2500583233.4
50002129425.8×3.65
100009066906.6×4.26
20000367731838.7×4.06

And the same table for append, from the same run, where the ratio is about two and the cost of a single operation does not move at all:

LengthTotal, µsns per operationAgainst the row above
25004919.7
500010120.3×2.06
1000020120.1×1.98
2000044122.0×2.19

The right-hand column is the whole difference. With append the cost of one operation is constant: about twenty nanoseconds at two and a half thousand elements and at twenty thousand alike. With insert(0) it grows with the length, because every insertion shifts everything already there.

measured observation

Every number in this section comes from one run of 3.13.7 and is compared only with the others in it. The absolute values are a property of the measuring machine (Intel Xeon 2.80 GHz, 2 vCPU), not of the language; what reproduces on other hardware is the ratio, not the nanosecond.

pop(0) against del L[0]: same work, different price

The change from Part II shows up in more than bytes.

Each operation below empties a ten-thousand-element list, inside a single run of 3.13.7 with bench/lists/ops.py:

OperationTotal, µsns per operation
L.pop()23623.6
del L[-1]21321.3
L.pop(0)12076612076.6
del L[0]173421734.2
d.pop()27227.2
d.popleft()26926.9

L.pop(0) costs seven times what del L[0] costs. Both remove the first element and both shift the tail; what separates them is the same path in the implementation — the general slice-assignment route against an in-place loop.

Look at the first two rows of the same table: from the END of the list those two paths cost 23.6 and 21.3 nanoseconds, a difference of a few per cent. At the end there is nothing to shift, so what shows is the per-call overhead alone — and that is nearly the same on both paths. The sevenfold gap appears where every call shifts the whole tail: what separates these two paths is not the checks before the work but what each of them does with the tail.

What this number cannot be used to claim is that the whole sevenfold difference falls on list_resize. The paths diverge in more than one place: pop also RETURNS the removed element, handing out a reference and looking after its lifetime, while del returns nothing. How much each of those costs is not something the measurement breaks down: it shows the total price of two different paths, and list_resize on one of them is a cause named from the source, not a measured summand.

The second thing this number cannot be used to claim is that "del got faster". That would compare time across versions, which this project forbids: the builds have different compilers and different flags, and nothing here separates "the language got faster" from "the build got faster". What can be claimed is exactly what was measured inside one run: on 3.13.7 two operations with the same meaning cost different amounts and leave the object in different states.

What deque charges for this

The standard answer to "I need both ends" is collections.deque, and the promise there is official:

Deques support thread-safe, memory efficient appends and pops from either side of the deque with approximately the same O(1) performance in either direction

The measurement agrees: for a deque, appendleft costs 29.4 nanoseconds against 29.3 for d.append, and popleft costs 26.9 against 27.2 for d.pop. Against nine hundred nanoseconds for insert(0) that is a different class of thing.

But the same documentation carries the other half: "Indexed access is O(1) at both ends but slows to O(n) in the middle". Here is what that costs on a hundred thousand elements, each expression repeated a thousand times per round — bench/lists/ops.py:

Expressionns per operation
L[0]7.5
L[50000]9.0
L[99999]9.1
d[0]11.6
d[50000]2418.3
d[99999]18.1

For the list the three numbers nearly coincide — the index does not depend on position. For the deque the middle costs hundreds of times what the ends cost: this is not a list with two cheap ends but a chain of BLOCKS — not a linked list of single elements and not an array, but a chain of chunks of several elements each, whose middle has to be walked to.

So the practical criterion is not about "which is faster" but about WHAT YOU DO with the container. Need access by arbitrary index — a list, and then the front is expensive. Need both ends and sequential traversal — a deque, and then the index is expensive. A list you keep taking the first element from is a deque written wrong.

Part IV. Practice

Practice · predict the output

Two identical lists; nine hundred elements are removed from the front of each — one through pop(0), the other through del. What does this code print on 3.13?
import struct
import sys


def capacity(seq):
  return (sys.getsizeof(seq) - sys.getsizeof([])) // struct.calcsize("P")


a = list(range(1000))
b = list(range(1000))

for _ in range(900):
  a.pop(0)

for _ in range(900):
  del b[0]

print(len(a), capacity(a))
print(len(b), capacity(b))

Practice · estimate

A ten-thousand-element list is emptied from the front, once through L.pop(0) and once through del L[0]. How many times more expensive is the first way on 3.13?
times

Part V. What to do with this

Four rules that follow from the measurements

Build a list at its final length when the length is known. list(range(n)), a literal and [0] * n take room for exactly the elements; a comprehension and a generator take it with slack. On one list that is forty-eight bytes, on a million it is forty-eight megabytes.

Do not work with the front of a list in a loop. The cost of one such operation grows with the length: at ten thousand elements an insertion at the front costs nine hundred nanoseconds against twenty for append, and a removal from the front costs 1734.2 nanoseconds through del L[0] and 12,076.6 through L.pop(0). If both ends are needed, use collections.deque; if an index into the middle is needed as well, what is needed is a different algorithm rather than a different container.

Rebuild a list that shrank a lot and lives on. L.copy() brings the capacity down to the length on all five builds checked, while del brings it down on none of the four builds from 3.13 onwards; on 3.12.3 it still did. And separately: a list gives back CAPACITY, not memory to the operating system — the memory allocator usually keeps the freed block, so the memory the process holds (its RSS) may not change after a rebuild.

Do not rely on shrinking, nor on its absence. No version promises when a list gives capacity back. What is measured here is the behaviour of five specific builds, and it has already changed once without a line in "What's New".

Version history

VersionChangeWhat this means for your code
3.12Deletion by index goes through list_ass_slice, which calls list_resize. del L[i] and L.pop(i) leave the object in the SAME state: after nine hundred removals from a thousand-element list the capacity is 184 for both.
3.13list_ass_item_lock_held appears, and list_resize is no longer called on the deletion-by-index path. del L[i] stops reducing capacity at all — from either end — while L.pop(i) keeps reducing it. "What's New" says nothing about this.
3.14Behaviour is as in 3.13: capacity after del does not shrink — and the same holds on the free-threaded 3.13.7t and 3.14.7t builds, where a list has its own allocation paths. The growth formula has never changed — the run of bench/lists/growth.py gives the same sequence of capacities and the same twenty-four jumps on all five builds.

How this was measured

The numbers in this article come from these scripts. Each opens from here, together with the record of its run.

Bytes and capacity — comparable across versions, recorded on all five builds, including two free-threaded ones:

Time — one record, one build:

The basis for the exercises in "Practice":

Python 3.12.3 (GCC 13.3.0), 3.13.7 (Clang 20.1.4), 3.14.7 (Clang 22.1.3) and the free-threaded 3.13.7t, 3.14.7t; Intel Xeon 2.80 GHz, 2 vCPU, 64-bit. Capacity and bytes are comparable across versions — that is object layout. Time is not: the builds have different compilers and different flags, and these measurements cannot separate "the language got faster" from "the build got faster". Time was taken on 3.13.7 with the GIL enabled only.

Fragments of Objects/listobject.c are quoted verbatim at tags v3.12.3 and v3.13.7.

Common misconceptions

Claim

A list never gives memory back

Actually

It gives capacity back, and the condition is one line inside list_resize: if (allocated >= newsize && newsize >= (allocated >> 1)) — while the length is at least half the capacity the array stays put, and once it falls below half the array is reallocated smaller. A thousand pop() calls from the end of a thousand-element list produce twelve such reductions. The opposite wording is wrong too: from 3.13 onwards del L[i] does not reduce capacity at all. And separately: reducing capacity is not returning memory to the operating system — the allocator usually keeps the freed block.

Claim

del L[0] and L.pop(0) are the same thing written two ways

Actually

On 3.12 they really are. On 3.13 and 3.14 they are not: after nine hundred removals from a thousand-element list, pop(0) leaves a capacity of 184 and del leaves 1000 — 1528 bytes against 8056. The length is a hundred either way, so the length shows nothing. The reason is that deletion by index no longer goes through list_ass_slice: from 3.13 it is done by list_ass_item_lock_held, where list_resize is never called. The same split shows in time within one run of 3.13.7: pop(0) costs seven times what del L[0] costs.

Claim

sys.getsizeof(L) tells you how much the list's elements take

Actually

It reports the size of the pointer array by ALLOCATED slots rather than by length, and it does not include the objects the pointers lead to. The function's documentation says so directly: Only the memory consumption directly attributed to the object is accounted for, not the memory consumption of objects it refers to. That is how you read a capacity that has no public name: (sys.getsizeof(L) - sys.getsizeof([])) // struct.calcsize("P").

Claim

Two lists with the same elements take the same memory

Actually

Only if built in a way that knows the length up front. A ten-element list takes 136 bytes when it is a literal, [0] * 10 or list(range(10)), and 184 bytes when it is a comprehension or list() of a generator: those two do not know the length in advance, elements arrive one at a time, and room is taken with slack — sixteen slots instead of ten.

Claim

append is cheap because the array simply gets longer

Actually

Sometimes it does get longer in place: realloc grows the allocation when there is free space past its end — on 3.13.7 with the GIL the run counted 13 such capacity jumps out of 24 over the first six hundred append calls, and 12 out of 24 on 3.12.3 and 3.14.7; the proportion depends on the state of the allocator, and what reproduces is only that both outcomes occur. But that cannot be counted on, and when there is no space the array moves in its entirety. append is cheap because room is over-allocated by the formula (n + (n >> 3) + 6) & ~3, so the over-allocation is proportional to the length. Over the first six hundred appends the array is reallocated twenty-four times, not six hundred. The comment in the source names the purpose: the over-allocation is there to give linear-time amortized behavior over a long sequence of appends().

Claim

collections.deque is a list with two cheap ends

Actually

It is a different structure with a different price. Both ends really are cheap — appendleft costs what append costs. But the index stops being constant: on a hundred thousand elements d[50000] costs 2418.3 nanoseconds against 9.0 for L[50000], hundreds of times more. The documentation promises both halves at once: Indexed access is O(1) at both ends but slows to O(n) in the middle.

Claim

The list growth formula is part of the language, so code may rely on it

Actually

It may not. The formula appears nowhere in the language documentation; it is a comment in Objects/listobject.c and an implementation detail of CPython. That it did not change between 3.12 and 3.14 is an observation from five runs, not a promise: the run of bench/lists/growth.py gives the same sequence of capacities and the same twenty-four jumps on 3.12.3, 3.13.7, 3.14.7 and the free-threaded 3.13.7t, 3.14.7t. What code may rely on is that append is cheap on average; particular capacity numbers, no.

Check yourself

Question 1 of 6

len(L) is 100. How much memory does the list take?

Sources & further reading

7 SOURCES

  1. Objects/listobject.c — list_resize and the growth formulaCPython source code. Where all list growth comes from. The comment above the function states both the purpose and the resulting sequence: “This over-allocates proportional to the list size, making room for additional growth. The over-allocation is mild, but is enough to give linear-time amortized behavior over a long sequence of appends()”. The formula itself is there too — `new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;` — along with the condition under which the array is LEFT as it is: `if (allocated >= newsize && newsize >= (allocated >> 1))`; shrinking begins where that condition fails. Read at tag v3.13.7.https://github.com/python/cpython/blob/v3.13.7/Objects/listobject.c
  2. Objects/listobject.c in 3.12.3 — deletion by index via list_ass_sliceCPython source code. For comparison with 3.13: before it, `list_ass_item` did nothing itself on deletion but handed the work to the general slice-assignment path — `if (v == NULL) return list_ass_slice(a, i, i+1, v);`. It is `list_ass_slice` that calls `list_resize` after shifting the tail, and that is where the capacity shrink came from — the one 3.13 no longer has.https://github.com/python/cpython/blob/v3.12.3/Objects/listobject.c
  3. Objects/listobject.c — list_pop_impl: which path pop takesCPython source code. Needed so that "pop goes through the general slice-assignment path" is not left as the author's word. The function splits two cases itself: when the LAST element is removed it calls `list_resize` directly — `if (index == Py_SIZE(self) - 1) { status = list_resize(self, Py_SIZE(self) - 1);`; in every other case, `pop(0)` included, the work is done by the general path — `status = list_ass_slice(self, index, index+1, (PyObject *)NULL);`. The second divergence from `del` is visible in the same place: a `Py_INCREF(v)` sits just before it, because the removed element is handed back out. Read in `Objects/listobject.c` on the 3.13 branch.https://github.com/python/cpython/blob/3.13/Objects/listobject.c
  4. Objects/listobject.c in 3.13.7 — list_ass_item_lock_heldCPython source code. The function in which the call to `list_resize` disappeared. It performs the deletion itself: a loop of `FT_ATOMIC_STORE_PTR_RELAXED(a->ob_item[idx], a->ob_item[idx + 1]);` followed by `Py_SET_SIZE(a, size - 1);`. Neither `list_resize` nor `list_ass_slice` appears on this path — hence the capacity staying put after `del`.https://github.com/python/cpython/blob/v3.13.7/Objects/listobject.c
  5. sys.getsizeof — what is actually returnedOfficial documentation. The sentence behind most misreadings of a list's size: “Only the memory consumption directly attributed to the object is accounted for, not the memory consumption of objects it refers to”. For a list, what is “directly attributed” is the array of pointers, and it is counted by ALLOCATED places rather than by length.https://docs.python.org/3/library/sys.html#sys.getsizeof
  6. collections.deque — what is actually promised about complexityOfficial documentation. The only place where the cost of working with both ends is officially promised: “Deques support thread-safe, memory efficient appends and pops from either side of the deque with approximately the same O(1) performance in either direction”. The price is stated in the same paragraph: “Indexed access is O(1) at both ends but slows to O(n) in the middle”.https://docs.python.org/3/library/collections.html#collections.deque
  7. What's New In Python 3.13Official documentation. Cited as a source of ABSENCE: the change in how `del L[i]` affects list capacity is not recorded in the document. Checked by reading the “Other Language Changes” and “Optimizations” sections. Hence this article's rule: the change is described as an observation with versions named, not as a documented decision.https://docs.python.org/3/whatsnew/3.13.html