Home / Python Fundamentals

Python List Lab

Interact with lists dynamically. Supports nested lists, negative indexing, and slicing complexity.

Operations

Tip: Use [ ] for nested lists


Snippet


                    

List State

my_list = [ ... ]
len: 4
Select an operation or click boxes to pop elements.

Insight

Python lists can contain heterogeneous data, including other lists (nesting).

  • • Nested Lists: Multi-dimensional structures like `[[1,2], [3,4]]`.
  • • Slicing: Creates a shallow copy of a sub-range.
  • • Center-Aligned: All items in the visualization are precisely centered.

Complexity Bar

Current Op Complexity O(1)

Overview

A Python list is a dynamic array: a contiguous block of references with spare capacity for growth. This gives fast indexing and amortized constant-time appends, while inserting near the front shifts later elements and takes linear time.

A dynamic array, not a linked list

OperationComplexityWhy
lst[i]O(1)Direct offset into the block
lst.append(x)O(1) amortisedWrites into spare capacity
lst.pop()O(1)Removes from the end
lst.insert(0, x)O(n)Shifts every element right
lst.pop(0)O(n)Shifts every element left
x in lstO(n)Linear scan

Indexing and slicing

Indices count from 0, and negative indices count from the end: lst[-1] is the last element, lst[-2] the second-last.

A slice takes [start:stop:step], where start is inclusive, stop is exclusive. And any part may be omitted. With lst = [10, 20, 30, 40, 50]:

lst[1:4]  → [20, 30, 40]   # stop is excluded

lst[:3]   → [10, 20, 30]

lst[::2]  → [10, 30, 50]   # every second element

lst[::-1] → [50, 40, 30, 20, 10] # reversed

The exclusive stop is deliberate: it makes lst[:k] + lst[k:] reconstruct the first for any k. And it makes the length of a slice simply stop − start. Every slice returns a new list, so slicing an n-element list costs O(n) time and O(n) memory. Slicing in a loop is a common accidental quadratic.

Copying, and the aliasing trap

A list holds references, so copying has two levels.

a = [1, 2, 3]
b = a               # NOT a copy - another name for the same list
b.append(4)
print(a)            # [1, 2, 3, 4]

c = a.copy()        # shallow copy - a new list, same objects inside
c = a[:]            # same thing
c = list(a)         # same thing

Shallow means the new list is separate, and the objects it points to are shared:

grid = [[0, 0], [0, 0]]
flat = grid.copy()
flat[0].append(9)
print(grid)         # [[0, 0, 9], [0, 0]] - the inner lists are shared

Use copy.deepcopy() when the nested objects must be free.

The same aliasing produces the most notorious Python gotcha:

board = [[0] * 3] * 3        # three references to ONE row
board[0][0] = 1
print(board)                 # [[1,0,0], [1,0,0], [1,0,0]]

board = [[0] * 3 for _ in range(3)]     # three separate rows - correct

[x] * n repeats the reference n times. For immutable values that is harmless; for mutable ones it is a bug.

Exploration guide

  1. Watch the exclusive stop. Set Start to 1 and Stop to 4. Then press Run Expression. Three elements come back, not four — index 4 is the boundary, not a member.
  2. Reverse with a negative step. Set Step to −1 and leave start and stop empty. The whole list comes back reversed, and it is a copy: the first is untouched.

What trips people up

  • The mutable default argument. def f(items=[]) creates the list once, when the function is defined, so calls share it. Use None as the default and build the list inside.
  • Removing items while iterating. Deleting shifts later elements and can make a loop skip items. Iterate over a copy or build a new list with a comprehension.
  • Using pop(0) as a queue. It shifts the remaining elements. Use collections.deque for efficient operations at both ends.

Sorting, and the in-place trap

items = [3, 1, 2]

items.sort()                  # sorts IN PLACE, returns None
new = sorted(items)           # returns a NEW sorted list

items = items.sort()          # BUG - items is now None

That last line is the classic mistake, and it follows a Python convention: methods that mutate return None, exactly so this error fails loudly rather than silently.

The key parameter is where the useful power is:

words.sort(key=len)                              # by length
people.sort(key=lambda p: (p.surname, p.first))  # by surname, then first name
items.sort(key=lambda x: -x.score)               # descending by score

Sorting by a tuple gives multi-key sorting in one pass. And because Timsort is stable, sorting by one key. Then another also works. The second sort preserves the first's order within ties, which is how "sort by department. Then by name within department" is done as two sorts.

Choosing the right structure

NeedUse
Indexed access, ordered itemslist
Add/remove at both endscollections.deque
Membership testingset — O(1) instead of O(n)
Key-value lookupdict
Fixed record, hashabletuple
Countingcollections.Counter
Groupingcollections.defaultdict(list)
Numeric arrays, mathsnumpy.ndarray
Sorted order maintainedbisect.insort, or sortedcontainers

The membership row is the one that most often matters:

# O(n^2) - 'in' scans the list for every item
for x in items:
    if x in other_list: ...

# O(n) - build a set once
other = set(other_list)
for x in items:
    if x in other: ...

For repeated membership checks, this avoids scanning the full list for each query.

Sources and next steps

Run it in Python

What a Python list actually is underneath — a resizable array of references — and the four consequences that follow, each measured: O(1) indexing, O(n) inserts at the front, over-allocated growth, and the aliasing trap.

lists.pyPython 3
Output

How the code works

  1. a[2]One multiplication and one memory read. The list stores references contiguously, so element i is at a computable address — this is the difference between a list and a linked list.
  2. lst.insert(0, 1)Everything after the insertion point moves one slot right, so this is O(n). Doing it in a loop is O(n²), and it is the usual reason a “queue” written on a list is slow — see queues.
  3. sys.getsizeof(lst)The size jumps in steps, not per item. CPython over-allocates on growth so that most appends need no reallocation, which is what “amortised O(1)” means concretely.
  4. [[0] * 3] * 3Multiplying a list repeats the reference three times, so all three rows are one object. This is the single most common Python bug in grid and matrix code.
  5. b = a versus c = a[:]Assignment binds another name to the same object; slicing builds a new one. is asks which object, == asks about contents, and confusing them is how mutation surprises happen.

Change one thing

  • Raise N to 60,000. The append timing doubles; the front-insert timing roughly quadruples.
  • Replace the front-insert loop with collections.deque and appendleft. Same result, back to linear.
  • Print sys.getsizeof for a list of 1,000 items and for 1,000 separate integers. The list stores references, so it is far smaller than the things in it.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Predict, then reveal

About to run: Watch the exclusive stop. Before it does — what happens to the readout?

Committing to an answer first is the point — the reveal runs the experiment on the visualisation above and reads the real value back, so nothing here is scripted.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Indexing a Python list is O(1) because the list stores:

  2. What does [[0] * 3] * 3 build?

  3. sys.getsizeof shows a list's size jumping in steps rather than per item because:

Cheat sheet

Python List Lab

Index, slice, append and mutate Python lists interactively, and see how the underlying dynamic array grows as you go.

ALGORITHMS · vizlearn.in/dsa/lists_in_python.html

Further reading

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.