Skip to content

Lists & Slicing Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on lists vs arrays, slicing semantics and negative steps, append vs extend vs insert, comprehensions, and sort vs sorted.

Read the in-depth guidePython list vs tuple vs set — Choosing the Right Data Structure(opens in new tab)
15 of 15

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel