skip to content

Collections and Data Structures

What list, dict, set and the collections module do at runtime, what each operation costs, and which one to reach for. Interviewers lean here because container choice is where Python turns quadratic.

part ofPythonoverview, primer and where to startread it →
on this pageshow

explore

questions

102 · 7 sections

Why does [[0] * 3] * 3 build a grid where setting one cell changes every row?

level: juniorimportance: must knowfreq 70%
basics
~10 s

Multiplying a list repeats references, not objects. The inner list is built once and its reference stored three times, so the three rows are one object: grid[0][0] = 1 appears in all of them.

open as a page

When should you choose a tuple over a list in Python, and why?

level: juniorimportance: must knowfreq 78%
basics
~20 s

Use a tuple for a fixed-shape record whose positions each mean something, and a list for a variable-length collection of like items. Tuples are also hashable, so a tuple can key a dict or join a set; a list cannot.

open as a page

Does putting a list inside a Python tuple make that list immutable?

level: juniorimportance: must knowfreq 62%
basics
~20 s

No. A tuple freezes only its own slots, meaning which objects they point at. A list stored in a tuple is still an ordinary mutable list, so appending to it works; only rebinding the slot fails.

open as a page

What is the difference between list.append() and list.extend() in Python?

level: juniorimportance: must knowfreq 76%
basics
~10 s

list.append(x) adds x to the end as one single element, so appending a list nests it. list.extend(iterable) walks the argument and adds each of its items separately, growing the list by that many elements.

open as a page

Why is Python's list.append amortized O(1) but list.insert(0, x) O(n)?

level: middleimportance: must knowfreq 65%
basics
~20 s

append usually writes into a spare slot the list already reserved, and the occasional resize copies pointers into a proportionally larger array, so the cost averages to constant. insert(0, x) shifts every existing pointer right, on every call.

open as a page

How do `dict.get` and `dict.setdefault` differ from indexing a Python dict with `d[key]`?

level: juniorimportance: must knowfreq 78%
basics
~20 s

Indexing a missing key raises KeyError. dict.get returns a default instead - None unless you pass one - and never changes the dict. dict.setdefault returns the existing value or inserts your default and returns that, so it mutates.

open as a page

Why does using a list as a dict key raise TypeError, and what works instead?

level: juniorimportance: must knowfreq 76%
basics
~20 s

A dict key must be hashable. Lists deliberately have no hash, because their contents can change and a key's hash must stay stable, so Python raises TypeError. Use a tuple of hashable items, or a frozenset when order is irrelevant.

open as a page

Since which Python version is dict insertion order a language guarantee?

level: juniorimportance: must knowfreq 70%
basics
~20 s

Python 3.7 made dict insertion order a language guarantee. CPython 3.6 already behaved that way as an implementation detail of its rebuilt table. Iterating a dict yields keys in the order they were first inserted.

open as a page

What does `dict.keys()` return in Python 3, and how does that object behave as the dict changes?

level: middleimportance: must knowfreq 60%
basics
~20 s

A dict_keys view object, not a list. The view is a live window onto the dict, so keys added or removed later show through it. It supports len, iteration, fast membership and set operations, but not indexing.

open as a page

Why is types.MappingProxyType only a shallow immutability guarantee?

level: middleimportance: must knowfreq 40%
basics
~20 s

It freezes the key-to-value bindings, not the objects the values point at. A list or nested dict fetched through the proxy can still be mutated in place, so a caller changes your state without ever assigning to a key.

open as a page

How do you deduplicate a list in Python while preserving first-seen order?

level: juniorimportance: must knowfreq 60%
basics
~10 s

list(dict.fromkeys(items)) keeps the first occurrence of each element in its original position, because dict iteration order is guaranteed insertion order since Python 3.7. set(items) also removes duplicates but iterates in an arbitrary order.

open as a page

Why can't a Python set contain a list, and how does frozenset help?

level: juniorimportance: must knowfreq 68%
basics
~20 s

A set stores its members in a hash table, so every element must be hashable, and lists are mutable and deliberately unhashable. frozenset is an immutable, hashable set, so it can sit inside another set or serve as a dict key.

open as a page

What is the difference between set.discard(x) and set.remove(x) in Python?

level: juniorimportance: must knowfreq 58%
basics
~20 s

Both delete the element when it is present. When it is absent, set.remove(x) raises KeyError while set.discard(x) does nothing and returns None. Use remove when a missing element is a bug, discard when absence is expected.

open as a page

Why does set.union accept a list argument when the | operator does not?

level: middleimportance: must knowfreq 45%
basics
~20 s

The operator forms are defined only between set and frozenset and raise TypeError on anything else, so mixed types fail loudly. The method forms are documented to accept any iterable and convert it as they go, and union, intersection and difference also take several iterables at once.

open as a page

In Python, how do you deduplicate a list of records by one field, keeping the first occurrence?

level: middleimportance: should knowfreq 42%
basics
~20 s

Track the derived key yourself: iterate the records, compute the field, and append a record only when its key is not already in a seen set. dict.fromkeys cannot do this, because it keys on the whole element.

open as a page

What does collections.Counter return when you look up a key that was never counted?

level: juniorimportance: must knowfreq 70%
basics
~20 s

Zero. Counter's missing hook returns 0 for any absent key without storing it, so the key stays out of the mapping, out of len and out of iteration. Only assigning to it or counting it creates an entry.

open as a page

What does collections.defaultdict(list) do that a plain dict does not?

level: juniorimportance: must knowfreq 70%
basics
~10 s

collections.defaultdict(list) calls its factory when a looked-up key is missing, stores the fresh empty list under that key and returns it, instead of raising KeyError. Grouping becomes a single append with no membership check.

open as a page

Why is `list.pop(0)` O(n) while `collections.deque.popleft()` is O(1)?

level: juniorimportance: must knowfreq 70%
basics
~20 s

A list keeps its items in one contiguous array, so removing index 0 shifts every remaining element down one slot. A deque is a doubly linked chain of small blocks, so popleft just unlinks the front item.

open as a page

What does collections.namedtuple give you that a plain tuple does not?

level: juniorimportance: must knowfreq 62%
basics
~10 s

collections.namedtuple is a class factory: it returns a new class that subclasses tuple and gives every position a name. You can read bid.cpm_cents as well as bid[1], and the repr prints field names.

open as a page

What does collections.OrderedDict still offer now that dict preserves insertion order?

level: middleimportance: must knowfreq 58%
basics
~20 s

Three things dict lacks: move_to_end(key, last=) to reposition a key in O(1), popitem(last=False) to pop the oldest entry, and order-sensitive equality between two OrderedDicts. Since Python 3.7 plain dict keeps insertion order, so ordering alone is no reason.

open as a page

Python's heapq is a min-heap - how do you get max-heap behaviour from it?

level: juniorimportance: must knowfreq 70%
basics
~20 s

heapq keeps the smallest item at index 0 and has no reverse switch. Negate the ordering key on the way in and back on the way out, or on Python 3.14 call its max-heap functions such as heapify_max and heappop_max.

open as a page

What is the difference between sorted() and list.sort() in Python?

level: juniorimportance: must knowfreq 85%
basics
~20 s

sorted() takes any iterable and returns a new sorted list, leaving the original untouched. list.sort() reorders an existing list in place and returns None. Both run the same stable sort and accept the same key and reverse arguments.

open as a page

How do bisect.bisect_left and bisect.bisect_right differ on duplicate values?

level: middleimportance: must knowfreq 55%
basics
~20 s

Both return an index where the value could be inserted keeping the list sorted. On a run of equal values bisect_left returns the index before the run and bisect_right just after it, bracketing the duplicates.

open as a page

Why can heapq.heappush raise TypeError when two entry tuples tie on priority?

level: middleimportance: must knowfreq 55%
basics
~20 s

Tuples compare element by element. When two priorities are equal, Python moves on to the next field and compares the payloads themselves; if those are dicts or plain objects with no ordering, the comparison raises TypeError.

open as a page

What does sorted()'s key argument receive, and how many times is it called?

level: middleimportance: must knowfreq 70%
basics
~20 s

key is a one-argument callable applied to each element exactly once, before any comparing happens; the sort then orders elements by those computed key values. For n elements it runs n times, not once per comparison.

open as a page

Which special methods let a custom class support len(obj), obj[k] and obj[k] = v?

level: juniorimportance: must knowfreq 70%
basics
~10 s

Python calls len for len(obj), getitem for reading obj[k], and setitem for assigning obj[k] = v. All three are looked up on the type, not the instance, and len must return a non-negative int.

open as a page

What must you implement to subclass collections.abc.MutableMapping, and what comes free?

level: middleimportance: must knowfreq 55%
basics
~10 s

Five methods: getitem, setitem, delitem, iter and len. From those five the base class derives get, membership, keys, items, values, pop, popitem, clear, update, setdefault and equality, so you write the storage once.

open as a page

Why must a `tuple` subclass set its contents in `__new__` rather than in `__init__`?

level: middleimportance: must knowfreq 45%
basics
~20 s

A tuple's items are fixed when the object is allocated, and allocation happens in __new__. __init__ runs afterwards and cannot change them, so a tuple subclass that defines only __init__ fails at construction with a TypeError from tuple.__new__.

open as a page

Why does dict.update() skip an overridden __setitem__ in a dict subclass?

level: middleimportance: must knowfreq 50%
basics
~20 s

Because dict.update() is C code that writes straight into the dictionary's internal storage instead of calling the Python-level setitem you defined. dict.init, setdefault and the |= operator behave the same way, so an override is honoured only for plain d[key] = value assignment.

open as a page

Why does slicing a `list` subclass return a plain `list` instead of the subclass?

level: juniorimportance: should knowfreq 35%
basics
~20 s

Built-in list operations construct their result with the concrete list type rather than with type(self), so slicing, +, * and copy() all hand back a plain list. Override those methods yourself, or wrap a list with collections.UserList.

open as a page

How does array.array store integers differently from a Python list?

level: juniorimportance: must knowfreq 40%
basics
~20 s

A Python list stores pointers to full int objects scattered on the heap. array.array stores raw machine values of one typecode packed back to back, so it holds only numbers of that type and costs far less memory.

open as a page

Why does slicing a memoryview of a bytearray avoid the copy that slicing the bytearray itself makes?

level: juniorimportance: must knowfreq 35%
basics
~20 s

Slicing a bytearray allocates a second bytearray and copies the bytes. A memoryview slice only records an offset and length into the same storage, so it costs the same tiny amount whatever the slice size.

open as a page

Why does appending 2**31 to an array.array('i') raise OverflowError?

level: middleimportance: should knowfreq 30%
basics
~20 s

The typecode 'i' means a signed C int, four bytes wide on CPython, so the largest value that fits is 2**31 - 1. array.array range-checks every value against the typecode and refuses the write instead of truncating it.

open as a page

Why does assigning into memoryview(b'abc') raise TypeError while memoryview(bytearray(b'abc')) allows it?

level: middleimportance: should knowfreq 40%
basics
~20 s

A bytes object is immutable, so the buffer it exports is flagged read-only and the view refuses writes with TypeError: cannot modify read-only memory. A bytearray exports writable memory, so its view can be written through into the original.

open as a page

Why does a live memoryview make bytearray.append raise BufferError, and how do you scope the view in a long-running ingest worker?

level: seniorimportance: should knowfreq 30%
basics
~10 s

A memoryview holds an export of the bytearray's storage, and a bytearray refuses to reallocate while any export is outstanding. So append, extend and clear raise BufferError; release the view before resizing.

open as a page