A list is Python's built-in, dynamically-sized, heterogeneous
container — it can hold objects of any type because each slot stores a
reference to a boxed Python object. The standard-library array.array
(and NumPy's ndarray) is homogeneous and typed, storing raw C values
contiguously, which is far more memory-efficient for large numeric data.
from array import array
nums = [1, 2, "three"] # list — mixed types allowed
typed = array("i", [1, 2, 3]) # array — all C ints, compact
typed.append("x") # TypeError — type-checked
import sys
sys.getsizeof([1, 2, 3]) # bigger: stores pointers
sys.getsizeof(array("i", [1, 2, 3])) # smaller: packed ints
Rule of thumb: reach for a plain list for general-purpose, mixed-type
collections; use array or NumPy when you have large amounts of
uniform numeric data and care about memory or vectorized speed.
A slice is lst[start:stop:step]. start is inclusive, stop is
exclusive, and any part can be omitted (defaults: start of list, end of
list, step 1). A negative step walks the list backwards, and when it's
negative the defaults for start/stop flip to the end and the
beginning.
a = [0, 1, 2, 3, 4, 5]
a[1:4] # [1, 2, 3] — stop is exclusive
a[:3] # [0, 1, 2] — omitted start = 0
a[::2] # [0, 2, 4] — every other element
a[::-1] # [5, 4, 3, 2, 1, 0] — reversed copy
a[4:1:-1] # [4, 3, 2] — backwards, stop exclusive
Slicing never raises for out-of-range indices (a[10:20] is just []),
unlike single-element indexing. Remember a[::-1] is the idiomatic way to get a
reversed shallow copy.
append(x) adds a single item to the end in amortized O(1).
extend(iterable) adds each element of an iterable to the end (also
amortized O(1) per element). insert(i, x) places an item at index i,
which is O(n) because every following element must shift right.
a = [1, 2, 3]
a.append([4, 5]) # [1, 2, 3, [4, 5]] — the LIST is one element
a = [1, 2, 3]
a.extend([4, 5]) # [1, 2, 3, 4, 5] — unpacks the iterable
a.insert(0, 99) # [99, 1, 2, 3, 4, 5] — O(n) shift
The classic trap: append([4, 5]) nests the whole list as one item, whereas
extend([4, 5]) merges its elements. Avoid insert(0, …) in a loop — it's
O(n) each time; use collections.deque for fast front insertion.
Usually yes. A comprehension runs its iteration in optimized C-level
bytecode and avoids the repeated list.append attribute lookup and method
call that a manual loop incurs, so it's typically 20–40% faster as well as
more concise. It also creates the loop variable in its own scope, so it
doesn't leak.
# manual loop — explicit append each iteration
squares = []
for n in range(1000):
squares.append(n * n)
# comprehension — faster and clearer
squares = [n * n for n in range(1000)]
Use a comprehension when you're building a list from an expression. But if
the body has side effects or grows complex (nested conditionals, multiple
statements), a readable for loop is the better choice — clarity beats a small
speed win.
list.sort() sorts a list in place and returns None — it mutates
the original and only works on lists. sorted(iterable) returns a new
sorted list and leaves the input untouched, accepting any iterable (tuples,
sets, generators, dict keys).
nums = [3, 1, 2]
result = nums.sort() # result is None! nums is now [1, 2, 3]
original = (3, 1, 2)
new = sorted(original) # new = [1, 2, 3], original unchanged
sorted(["bb", "a"], key=len, reverse=True) # ['bb', 'a']
Both are stable (equal elements keep their order) and run in O(n log n)
(Timsort). Watch the gotcha: x = mylist.sort() leaves x as None. Use
sort() to save memory when you don't need the original; use sorted() when you
must preserve it or are sorting a non-list iterable.
List multiplication copies references, not objects. [[]] * 3 makes three
slots pointing at the same inner list, so mutating one mutates all. Build
independent rows with a comprehension instead.
grid = [[0]] * 3
grid[0].append(1) # [[1], [1], [1]] ! all share one list
grid = [[0] for _ in range(3)] # correct: 3 distinct lists
Rule of thumb: * is safe for immutables ([0] * 3) but never for nested
mutable containers — use a comprehension there.
The iterator tracks an index that keeps advancing while the list shrinks,
so removing an element makes the loop skip the next one. Iterate over a
copy, or build a new list with a comprehension / filter.
nums = [1, 2, 2, 3]
for n in nums[:]: # iterate a copy
if n == 2:
nums.remove(n)
# cleaner: nums = [n for n in nums if n != 2]
Rule of thumb: don't mutate a list you're looping over — filter into a new list or iterate a slice copy.
list(x), x[:], and x.copy() make a shallow copy — a new outer list
whose elements are the same objects. Nested mutable elements are still
shared. Use copy.deepcopy to recursively copy everything.
import copy
a = [[1], [2]]
b = a[:] # shallow
b[0].append(9) # affects a too -> a == [[1, 9], [2]]
c = copy.deepcopy(a) # fully independent
Rule of thumb: shallow copy is enough for flat lists; deep-copy when nested mutable objects must be independent.
Negative indices count from the end: -1 is the last element, -2 the
second-to-last, and so on. It's shorthand to avoid len(x) - 1. Going beyond
the start (-len-1) raises IndexError, but slicing never does.
x = [10, 20, 30]
x[-1] # 30
x[-2] # 20
x[-5] # IndexError
x[-5:] # [10, 20, 30] -> slice clamps, no error
Rule of thumb: use negative indices for "from the end" access; prefer slicing when out-of-range should be tolerated.
Use enumerate(seq, start=0), which yields (index, value) pairs — far
cleaner and less error-prone than range(len(seq)) plus manual indexing. The
start argument sets the first index.
for i, name in enumerate(["a", "b"], start=1):
print(i, name) # 1 a / 2 b
Rule of thumb: never write for i in range(len(x)): x[i] — use enumerate
when you need the index alongside the value.
As a stack (LIFO): yes — append/pop at the end are O(1). As a
queue (FIFO): technically append + pop(0), but pop(0) and
insert(0, x) are O(n) because every element shifts. Use
collections.deque for an efficient queue.
stack = []; stack.append(1); stack.pop() # O(1) both
from collections import deque
q = deque(); q.append(1); q.popleft() # O(1) FIFO
Rule of thumb: list is a fine stack; use deque whenever you pop/insert at
the front.
O(n) — membership scans the list element by element. For repeated
membership tests on large data, convert to a set (O(1) average) or dict
first. The convenience of in on a list hides a linear cost.
if target in big_list: # O(n) each call
...
lookup = set(big_list) # O(n) once
if target in lookup: # O(1) thereafter
...
Rule of thumb: a one-off in on a list is fine; for many lookups, build a set
or dict.
A *name in an unpacking target captures the "rest" as a list. It can sit
anywhere in the pattern — start, middle, or end — grabbing everything not
matched by the fixed names.
first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3] # init=[1, 2], last=3
a, *mid, b = [1, 2, 3, 4] # a=1, mid=[2, 3], b=4
Rule of thumb: use starred unpacking to split off head/tail elements without slicing index arithmetic.
Pass a key function returning the sort value; add reverse=True to
descend. For multiple criteria return a tuple — Python sorts
lexicographically and the sort is stable, so equal keys keep their
original order.
people.sort(key=lambda p: p.age)
people.sort(key=lambda p: (p.last, p.first)) # by last, then first
from operator import attrgetter
people.sort(key=attrgetter("age"), reverse=True) # fast, descending
Rule of thumb: use key (with operator.attrgetter/itemgetter for speed)
and a tuple key for multi-level sorting.
Use the bisect module: bisect.insort(lst, x) inserts x keeping the
list sorted, and bisect.bisect_left/right finds the insertion point via
binary search (O(log n)). The insert itself is still O(n) due to shifting,
but you avoid re-sorting the whole list each time.
import bisect
data = [1, 3, 5]
bisect.insort(data, 4) # [1, 3, 4, 5]
bisect.bisect_left(data, 4) # 2 -> index where 4 sits
Rule of thumb: use bisect for ordered inserts and fast lookups in a sorted
list instead of appending and re-sorting.
More Data Structures interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.