Skip to content
Laviyx Learn

Try

DSA course menu

Arrays and Dynamic Arrays: How Python Lists Work

Learn how arrays store data in contiguous memory, why indexing is O(1), how dynamic arrays like Python lists grow by resizing, what amortized O(1) append means, and the cost of inserting and deleting.

Beginner7 min readUpdated
On this page

An array stores items next to each other in one block of memory. Because every slot is the same size, the computer can jump straight to item i with simple arithmetic:

Text
address of item i = start address + i × slot size

That is why reading items[i] is O(1): no searching, just one calculation. Python's list is an array of references to objects, and everything in this lesson applies to it.

Operation costs#

OperationCostWhy
read or write a[i]O(1)address arithmetic
append at the endO(1) amortizedusually a free slot is waiting
pop from the endO(1)nothing moves
insert or delete at position iO(n)every later item shifts by one
search for a valueO(n)items must be checked one by one

Why inserting in the middle is slow#

Inserting at the front means every existing item moves one place right. Here is what insert does internally, written out by hand:

Python
def insert_at(items, index, value):
    items.append(None)                 # make room at the end
    moves = 0
    for i in range(len(items) - 1, index, -1):
        items[i] = items[i - 1]        # shift right
        moves += 1
    items[index] = value
    return moves

data = [10, 20, 30, 40, 50]
print(insert_at(data, 0, 5), "moves ->", data)
print(insert_at(data, 6, 60), "moves ->", data)
Output
5 moves -> [5, 10, 20, 30, 40, 50]
0 moves -> [5, 10, 20, 30, 40, 50, 60]

Inserting at the front moved all 5 items; inserting at the end moved none. Deleting is the mirror image: later items shift left to close the gap.

Dynamic arrays: how lists grow#

A plain array has a fixed size. Python lists can grow because they are dynamic arrays: they keep some spare capacity, and when it runs out they allocate a bigger block and copy everything across.

This small class does the same thing and counts the copying:

Python
class DynamicArray:
    def __init__(self):
        self.capacity = 1
        self.size = 0
        self.slots = [None] * self.capacity
        self.copies = 0

    def append(self, value):
        if self.size == self.capacity:          # full: grow
            self.capacity *= 2
            bigger = [None] * self.capacity
            for i in range(self.size):
                bigger[i] = self.slots[i]
                self.copies += 1
            self.slots = bigger
        self.slots[self.size] = value
        self.size += 1

arr = DynamicArray()
for n in range(1, 17):
    arr.append(n)
    if n in (1, 2, 3, 5, 9, 16):
        print(f"size {arr.size:>2}, capacity {arr.capacity:>2}, total copies so far {arr.copies}")
Output
size  1, capacity  1, total copies so far 0
size  2, capacity  2, total copies so far 1
size  3, capacity  4, total copies so far 3
size  5, capacity  8, total copies so far 7
size  9, capacity 16, total copies so far 15
size 16, capacity 16, total copies so far 15

Resizes happen at sizes 1, 2, 4, 8, … The occasional resize is O(n), but they are so rare that 16 appends caused only 15 copies in total: less than one copy per append on average. This is what amortized O(1) means: individual appends may be slow, but the average cost per append stays constant.

2D arrays#

A grid is an array of arrays. Build each row separately:

Python
rows, cols = 3, 4
grid = [[0] * cols for _ in range(rows)]
grid[1][2] = 7
for row in grid:
    print(row)
Output
[0, 0, 0, 0]
[0, 0, 7, 0]
[0, 0, 0, 0]

Common mistakes#

Practice#

Practice exercise

Write remove_all(items, value) that removes every occurrence of value from the list in place in O(n) time, and returns the new length. Do not call remove (each call is O(n), so a loop of them is O(n²)).

Show hint

Keep a "write" index. Walk through the list with a "read" index; whenever the item is not value, copy it to the write position and move the write index forward. At the end, cut the list at the write index.

Show solution
Python
def remove_all(items, value):
    write = 0
    for read in range(len(items)):
        if items[read] != value:
            items[write] = items[read]
            write += 1
    del items[write:]          # drop the leftover tail
    return write

data = [3, 1, 3, 2, 3, 4]
print(remove_all(data, 3), data)
Output
3 [1, 2, 4]

Each item is read once and written at most once: O(n) time, O(1) extra space. This read/write pointer idea is a form of the two pointers pattern.

Key takeaway#

Next: arrays of characters, in Strings.

Finished this lesson?