Skip to Content
Lists

Lists

Lists are ordered, mutable sequences that can hold mixed types.

Lists show up everywhere in Python - and in interviews. The patterns you learn here (indexing, slicing, methods, comprehensions) carry over to tuples, sets, and dictionaries, so time spent on lists pays off across the language.

Creating Lists

You can build a list in several ways:

ApproachExampleUse when
Literal []row = [2, 4, 6]You know the elements
Empty listseq = []Start empty, append or extend later
list()list()[]Empty list (same as [])
list(iterable)list(range(4))[0, 1, 2, 3]Convert from another iterable (range, str, tuple, set)

Length: Use len(lst) to get the number of elements. For lists this is O(1); the length is stored.

Truthiness: An empty list [] is falsy; a non-empty list is truthy. Use if lst: to mean “if the list has at least one element” and if not lst: for “if the list is empty.”

Example:

row = [2, 4, 6, 8] empty = [] from_range = list(range(0, 6, 2)) # [0, 2, 4] from_string = list("ab") # ['a', 'b'] len(row) # 4 len(empty) # 0 if row: # True if not empty: # True

List Methods

MethodDescription
count()Count occurrences of a value
index()Return index of first occurrence; raises if missing
append()Add element at the end
insert()Insert element at index
extend()Append all items from an iterable
remove()Remove first occurrence of value
pop()Remove and return element at index (default last)
clear()Remove all elements
copy()Return a shallow copy
reverse()Reverse order in place
sort()Sort in place

Access List Items

Use indexing and slicing to read elements. Indices start at 0; negative indices count from the end.

PatternDescriptionExample
lst[i]Single element at index ivals[2] → element at index 2
lst[-1]Last elementvals[-1] → last element
lst[i:j]Slice from i to j (excl.)vals[2:6] → indices 2, 3, 4, 5
lst[:n]First n elementsvals[:4] → indices 0–3
lst[n:]From index n to endvals[4:] → indices 4 onward
lst[-m:-n]Slice with negative indicesvals[-4:-2] → 4th and 3rd from end

Slicing with step: The full form is lst[start:stop:step]. step is the stride; omit it and it defaults to 1. Use a negative step to walk backward (e.g. lst[::-1] for a reversed copy).

PatternDescriptionExample
lst[::k]Every k-th elementvals[::2][2, 6, 10, 14]
lst[i::k]Every k-th from index ivals[1::2][4, 8, 12, 16]
lst[::-1]Reversed (step -1)vals[::-1] → reversed copy

Membership: Use in to check whether an element exists in a list.

Value lookup: Use count(x) to count occurrences and index(x) to get the first index (raises ValueError if missing).

Note: index(x) raises ValueError when the item is not present in the list. Use in to check membership first, or wrap in try/except if you need to handle the missing case.

Example:

vals = [2, 4, 6, 8, 10, 12, 14, 16] vals[2] # 6 vals[-1] # 16 vals[2:6] # [6, 8, 10, 12] vals[:4] # [2, 4, 6, 8] vals[4:] # [10, 12, 14, 16] vals[-4:-2] # [12, 14] 4 in vals # True vals.count(6) # 1 vals.index(8) # 3

Reversing a List

MethodReturnsMutates original?
lst[::-1]New listNo
list(reversed(lst))New listNo
lst.reverse()NoneYes (in place)
  • lst[::-1] - Slice with step -1; returns a new reversed list.
  • list(reversed(lst)) - reversed() returns a reverse iterator; wrap in list() to get a list.
  • lst.reverse() - Reverses in place and returns None. To keep the original, copy first: lst_copy = lst.copy(); lst_copy.reverse().
row = [2, 4, 6, 8] # New list; original unchanged reversed_slice = row[::-1] # [8, 6, 4, 2] reversed_builtin = list(reversed(row)) # [8, 6, 4, 2] print(row) # [2, 4, 6, 8] - unchanged # In place; returns None row.reverse() # None print(row) # [8, 6, 4, 2] - mutated # To keep the original and get a reversed copy: original = [2, 4, 6, 8] copy_then_reverse = original.copy() copy_then_reverse.reverse() # copy_then_reverse is [8, 6, 4, 2]; original still [2, 4, 6, 8]

Unpacking Lists

Assign multiple names from a list in one line. The number of names on the left must match the length of the list, unless you use * to collect the rest.

PatternExampleResult / note
Same lengtha, b, c = [2, 4, 6]a=2, b=4, c=6
*restfirst, *rest = [2, 4, 6, 8]first=2, rest=[4, 6, 8]
*midfirst, *mid, last = [2, 4, 6]first=2, mid=[4], last=6
Index with loopfor i, x in enumerate(row):i = index, x = element
Spread into list[*row, 10, 12]New list: elements of row plus 10, 12

* in unpacking: One starred name can appear and receives zero or more elements as a list. Use it when the length is variable or you want “first” and “rest” (or “first, middle, last”) without indexing.

Example:

row = [2, 4, 6, 8] a, b, c, d = row # a=2, b=4, c=6, d=8 head, *tail = row # head=2, tail=[4, 6, 8] first, *mid, last = row # first=2, mid=[4, 6], last=8 for i, x in enumerate(row): print(i, x) # 0 2, 1 4, 2 6, 3 8 combined = [*row, 10, 12] # [2, 4, 6, 8, 10, 12]

Add List Items

MethodDescription
lst.append(x)Add x at the end
lst.insert(i, x)Insert x at index i
lst.extend(iter)Append all items from an iterable (list, tuple)

Note: extend() works with any iterable - tuples, sets, ranges, etc.

+ vs extend()

Aspectlst + otherlst.extend(other)
ReturnsNew listNone (mutates)
Mutates originalNoYes
Operand typesBoth must be listsother = any iterable
Example[1,2] + [3,4][1,2,3,4]lst.extend([3,4])lst becomes [1,2,3,4]
  • + creates a new list; both operands must be lists.
  • extend() mutates in place and returns None; accepts any iterable (tuple, set, range, etc.).
  • Use + when you want a new list; use extend() when you want to grow the list in place.

+ with non-list: If other is not a list (e.g. tuple, set, dict), lst + other raises TypeError: can only concatenate list (not "tuple") to list. Use extend() or convert first: lst + list(other).

extend() with different iterables

IterableExampleResultOrder
listlst.extend([8, 10])[2, 4, 6, 8, 10]Preserved
tuplelst.extend((8, 10))[2, 4, 6, 8, 10]Preserved
setlst.extend({8, 10})[2, 4, 6, 8, 10]May vary (set is unordered)
dictlst.extend({8: "a", 10: "b"})[2, 4, 6, 8, 10]Preserved (keys, insertion order)

Dict: Iterating over a dict yields its keys only. extend(d) adds the keys to the list, not the values or key-value pairs. Use d.values() or d.items() if you need values or pairs.

Example:

evens = [2, 4, 6] evens.append(8) # [2, 4, 6, 8] evens.insert(1, 0) # [2, 0, 4, 6, 8] more = [10, 12] evens.extend(more) # [2, 0, 4, 6, 8, 10, 12] evens.extend((14, 16)) # [2, 0, 4, 6, 8, 10, 12, 14, 16]

Change List Items

Lists are mutable. Assign to an index or slice to update in place.

OperationMethodEffect
Single indexlst[i] = xReplace element at i
Slice replacelst[i:j] = [...]Replace slice; length can differ
Insert at indexlst.insert(i, x)Insert x before index i

Example:

colors = ["red", "blue", "green", "yellow", "black"] colors[1:4] = ["pink", "cyan"] # ["red", "pink", "cyan", "black"] - fewer items colors[2] = "purple" # ["red", "pink", "purple", "black"] colors[1:2] = ["mint", "navy"] # ["red", "mint", "navy", "purple", "black"] - more items colors.insert(3, "olive") # ["red", "mint", "navy", "olive", "purple", "black"]

Remove List Items

Method / KeywordDescription
lst.remove(x)Remove the first occurrence of x; raises ValueError if missing
lst.pop(i)Remove and return the element at index i; default -1 (last)
del lst[i]Delete element at index i (or slice i:j)
del lstRemove the variable binding; list may be garbage-collected
lst.clear()Remove all elements in place; list becomes []

Example:

data = [2, 4, 6, 4, 8] data.remove(4) # [2, 6, 4, 8] - first 4 removed data.pop(2) # returns 4; data = [2, 6, 8] data.pop() # returns 8; data = [2, 6] # data.remove(99) # ValueError if 99 not in list # data.pop(10) # IndexError if index out of range # del data[10] # IndexError if index out of range del data[0] # data = [6] data = [2, 4, 6, 8, 10] del data[1:4] # data = [2, 10] - slice removed data.clear() # data = []

remove() vs pop()

Aspectlst.remove(x)lst.pop(i)
Identifies byValue xIndex i; default -1 (last)
ReturnsNoneThe removed element
If missingValueErrorIndexError (index out of range)
Use whenYou know the value to removeYou know the index or want the last item
  • lst.remove(x) removes the first occurrence of x by value; raises ValueError if not found.
  • lst.pop(i) removes and returns the element at index i; use when you need the removed value or know the position.

Example (remove vs pop):

items = [10, 20, 30, 20, 40] items.remove(20) # [10, 30, 20, 40] - first 20 removed; returns None last = items.pop() # last = 40; items = [10, 30, 20] mid = items.pop(1) # mid = 30; items = [10, 20] # items.remove(99) # ValueError: list.remove(x): x not in list # items.pop(10) # IndexError: pop index out of range

Remove all occurrences of a value:

  • remove() only removes the first.
  • Use a list comprehension for a new list: lst = [x for x in lst if x != value].
  • To mutate in place: lst[:] = [x for x in lst if x != value], or while value in lst: lst.remove(value) (simpler but O(n²)).
data = [2, 4, 6, 4, 8] data = [x for x in data if x != 4] # [2, 6, 8] - new list; O(n) # Or in place: data[:] = [x for x in data if x != 4] # O(n) # Or: while 4 in data: data.remove(4) # O(n²)

lst = [...] vs lst[:] = [...]

Aspectlst = [...]lst[:] = [...]
EffectRebinds variable to new listReplaces contents of existing list in place
List objectNew object; original unchangedSame object; elements changed
Other refsStill see old listSee updated list
Use whenWant a new list; other refs unchangedWant to keep all references in sync
  • lst = [...] creates a new list and rebinds lst to it; the original list object is unchanged and other names still see the old contents.
  • lst[:] = [...] replaces the contents of the existing list in place; the list object stays the same and all references see the updated contents.

Example:

original = [1, 2, 2, 3] ref = original original = [x for x in original if x != 2] # rebind # original = [1, 3], ref = [1, 2, 2, 3] - ref still sees old list original = [1, 2, 2, 3] ref = original original[:] = [x for x in original if x != 2] # replace contents # original = [1, 3], ref = [1, 3] - same object, both updated

What a[:] means: a[:] is a slice that selects the whole list. In a[start:stop], when both are omitted: start defaults to 0, stop defaults to len(a). So a[:] = a[0:len(a)] - every element from start to end.

ContextMeaning
Read (b = a[:])Shallow copy of the list
Write (a[:] = [1, 2, 3])Replace all elements in place

Assigning to a[:] uses slice assignment: it replaces the elements in the slice with the right-hand side. Because the slice is the whole list, the entire contents are replaced, but the list object itself stays the same.

clear() vs del lst

Aspectlst.clear()del lst
EffectEmpties list in place; object staysRemoves variable binding; list may be garbage-collected
List objectRemains (now [])May be garbage-collected if no other refs
Use whenEmpty list; other refs still existRemove the variable entirely
  • lst.clear() empties the list in place; the list object remains and other references still see the empty list.
  • del lst removes the variable binding; the list may be garbage-collected if no other references exist.

Note: We say “may be garbage-collected” because garbage collection in Python is not immediate or guaranteed. The list is only collected when the interpreter runs GC (typically when there are no more references). CPython uses reference counting and often frees objects quickly, but the language does not specify when GC runs.

Example (clear vs del):

a = [1, 2, 3] b = a a.clear() # a = [], b = [] - same object, now empty # del a # would remove name a; b still refers to [] c = [4, 5, 6] del c # c is gone; list may be garbage-collected

Loop Lists

Three common patterns: direct iteration, index-based, and while loop.

Example:

nums = [2, 4, 6, 8] for x in nums: print(x) # 2, 4, 6, 8 for i in range(len(nums)): print(nums[i]) # 2, 4, 6, 8 i = 0 while i < len(nums): print(nums[i]) i += 2 # 2, 6 - step by 2

Loop in reverse:

nums = [2, 4, 6, 8] for x in reversed(nums): # O(n) print(x) # 8, 6, 4, 2 for x in nums[::-1]: # O(n) print(x) # 8, 6, 4, 2 - slice with step -1 for i in range(len(nums) - 1, -1, -1): # O(n) print(nums[i]) # 8, 6, 4, 2 - index-based reverse
  • reversed(lst) - Returns a reverse iterator; does not create a new list.
  • lst[::-1] - Slice with step -1; creates a new list in reverse order.
  • range(len(lst)-1, -1, -1) - Index-based; iterate from last index down to 0.

Why LeetCode often prefers the index-based reverse: When you need to modify elements by index (swap, overwrite, remove), you must have the index. reversed() and lst[::-1] give values, not indices. The range(len(lst)-1, -1, -1) pattern also avoids index shifting when removing elements in place, and it translates directly to other languages (C, Java, etc.).

Comprehension

Build a new list from an iterable with an optional filter and expression.

Syntax: [expr for item in iterable if condition]

  • expr - What to put in the new list
  • iterable - Source (list, range, tuple, etc.)
  • condition - Optional filter; only items where it is truthy are included

Example:

evens = [2, 4, 6, 8, 10] doubled = [x * 2 for x in evens] # [4, 8, 12, 16, 20] filtered = [x for x in evens if x > 4] # [6, 8, 10] from_range = [x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8] squared = [x ** 2 for x in evens] # [4, 16, 36, 64, 100] - build list from modified elements

Copy Lists

Assignment does not copy; it creates another reference to the same list.

Shallow Copy

A shallow copy creates a new list, but nested objects (e.g. inner lists) remain shared references. Changes to nested objects affect both the original and the copy.

When to use shallow copy: The list is flat or contains only immutable elements (ints, strings, tuples). Shallow copy is faster and sufficient for most cases.

ApproachExampleUsage
lst.copy()new = orig.copy()b = a.copy() → new list
list(lst)new = list(orig)c = list(a) → new list
Slice [:]new = orig[:]d = a[:] → new list

All three are equivalent for lists - they produce the same shallow copy. The difference is style and intent:

ApproachProsCons
lst.copy()Explicit, clear intent; preferred for listsOnly on types with copy() (list, dict, set) - not tuple, range, str
list(lst)Works on any iterable (tuple, range, etc.)Reads as “build from iterable,” not “copy” - intent less explicit
lst[:]Concise; common in older codeIntent less obvious; slice semantics

Preference: Use lst.copy() when copying a list - it was added (Python 3.3) to replace lst[:] and makes intent clear. Use list(iterable) when converting from another iterable. lst[:] still works but is less preferred for readability. Performance is effectively the same.

Visualization

Shallow copy creates a new outer list, but inner objects are shared.

original = [[1, 2], [3, 4]] shallow = original.copy() original ────► [ref₁, ref₂] │ │ shallow ────► [ref₁, ref₂] ← same refs (shared) │ │ ▼ ▼ [1, 2] [3, 4] ← shared (single instance of each)

Nested change - shallow[0].append(99) modifies the shared [1, 2], so original[0] also becomes [1, 2, 99].

First-level add - shallow.append([5, 6]) adds to shallow’s outer list only; original unchanged.

original ────► [ref₁, ref₂] [1, 2] [3, 4] shallow ────► [ref₁, ref₂, ref₅] ref₅ ──► [5, 6] ← new; original has no ref₅

First-level remove - shallow.pop() removes from shallow only; original unchanged.

Deep Copy

A deep copy recursively copies the list and all nested objects. The copy is fully independent; changes to nested structures do not affect the original.

When to use deep copy: The list contains nested mutable structures (lists, dicts, custom objects) and you need to modify the copy without affecting the original.

ApproachExample
copy.deepcopy(lst)new = copy.deepcopy(orig)

Visualization

Deep copy recursively duplicates the list and all nested objects.

original = [[1, 2], [3, 4]] deep = copy.deepcopy(original) original ──► [ref₁, ref₂] ref₁ ──► [1, 2] (outer list) ref₂ ──► [3, 4] deep ──► [ref₃, ref₄] ref₃ ──► [1, 2] ← separate copies (new outer) ref₄ ──► [3, 4]

Nested change - deep[0].append(100) only changes the copy; original stays unchanged.

First-level add - deep.append([5, 6]) adds to deep’s outer list only; original unchanged.

original ──► [ref₁, ref₂] ref₁ ──► [1, 2] ref₂ ──► [3, 4] deep ──► [ref₃, ref₄, ref₅] ref₃ ──► [1, 2] ref₄ ──► [3, 4] ref₅ ──► [5, 6]

First-level remove - deep.pop() removes from deep only; original unchanged.

Summary: First-level operations (append, pop, remove at the outer list) are independent for both shallow and deep copy. The difference appears only when modifying nested objects.

Example:

import copy # Shallow copy - fine for flat lists a = [2, 4, 6] b = a.copy() # b = [2, 4, 6] c = list(a) # c = [2, 4, 6] d = a[:] # d = [2, 4, 6] a.append(8) # a = [2, 4, 6, 8]; b, c, d unchanged # Shallow copy - nested lists are shared original = [[1, 2], [3, 4]] shallow = original.copy() shallow[0].append(99) # original[0] is also [1, 2, 99] - shared reference # First-level add - independent (each has its own outer list) shallow.append([5, 6]) # original = [[1,2,99],[3,4]]; shallow = [[1,2,99],[3,4],[5,6]] # First-level remove - independent shallow.pop() # original unchanged; shallow = [[1,2,99],[3,4]] # Deep copy - fully independent deep = copy.deepcopy(original) deep[0].append(100) # original unchanged; deep[0] = [1, 2, 99, 100] deep.pop() # original unchanged; deep = [[1,2,99,100]]

Sort Lists

Method / Built-inDescription
lst.sort()Sort in place; returns None
lst.sort(reverse=True)Sort descending in place
lst.sort(key=fn)Sort by key function (e.g. abs, str.lower)
lst.reverse()Reverse order in place
reversed(iterable)Returns reverse iterator; use list(reversed(lst)) for a new list

Note: sort() is case-sensitive by default. Use key=str.lower for case-insensitive string sorting.

Example:

nums = [22, 4, 16, 8, 10] nums.sort() # [4, 8, 10, 16, 22] nums.sort(reverse=True) # [22, 16, 10, 8, 4] labels = ["Beta", "alpha", "Gamma"] labels.sort() # ['Beta', 'Gamma', 'alpha'] labels = ["Beta", "alpha", "Gamma"] labels.sort(key=str.lower) # ["alpha", "Beta", "Gamma"] labels.reverse() # ["Gamma", "Beta", "alpha"] nums = [1, 2, 3] print(list(reversed(nums))) # [3, 2, 1]; nums unchanged nums = [22, 4, 16, 8, 10] # abs(22-12)=10, abs(4-12)=8, abs(16-12)=4, abs(8-12)=4, abs(10-12)=2 → [10, 8, 4, 4, 2] → sort by keys → [10, 16, 8, 4, 22] nums.sort(key=lambda n: abs(n - 12)) # [10, 16, 8, 4, 22]

Why list(reversed(lst)) and not just reversed(lst): reversed() returns a reverse iterator, not a list. Returning reversed(lst) alone gives an iterator object; wrap it in list() to get a list.

lst.sort(reverse=True) vs lst.reverse()

Aspectlst.sort(reverse=True)lst.reverse()
EffectSorts descending (by value)Reverses element order only
LogicCompares elements; reorders by valueFlips indices; no comparison
Use whenWant descending sortWant reverse of current order
  • sort(reverse=True) sorts the list in descending order. For [3, 1, 2][3, 2, 1].
  • reverse() reverses the order without sorting. For [3, 1, 2][2, 1, 3].

If the list is already sorted ascending, both yield the same result. Otherwise they differ.

sort() vs sorted()

Aspectlst.sort()sorted(lst)
Modifies listYes (in place)No (original unchanged)
Return valueNoneNew sorted list
Works onLists onlyAny iterable (list, tuple, str…)
key / reverseSame: lst.sort(key=fn, reverse=True)Same: sorted(lst, key=fn, reverse=True)

Use sort() when you own the list and can mutate it. Use sorted() when you must keep the original, when the source is not a list (e.g. tuple, range), or when you want the result in an expression: result = sorted(items, key=len).

Example:

row = [6, 2, 8, 4] row.sort() # row is now [2, 4, 6, 8]; returns None out = sorted(row) # out = [2, 4, 6, 8]; row unchanged from_tuple = sorted((4, 2, 8)) # [2, 4, 8] - input can be any iterable

Lambda vs Built-in / Method

  • Use built-in or method when one exists: str.lower is already a function that takes one argument; str.lower("Beta") returns "beta". Pass it directly: key=str.lower.
  • Use lambda when you need custom logic: There is no built-in “distance from 12”. Use key=lambda n: abs(n - 12).
  • Rule of thumb: If a suitable function exists (len, str.lower, abs), pass it directly. If not, use a lambda or a named function.

The key Parameter

For each element x, Python computes key(x) and compares those values instead of the elements themselves. It creates an intermediate array of keys (e.g. [10, 8, 4, 4, 2]), sorts by comparing those keys, and reorders the original elements accordingly - it does not compare the original elements directly. Equivalent long-form:

Example - step-by-step equivalent of words.sort(key=len):

words = ["apple", "pie", "a", "zebra"] pairs = [(len(x), x) for x in words] # [(5, 'apple'), (3, 'pie'), (1, 'a'), (5, 'zebra')] pairs.sort(key=lambda p: p[0]) # sort by key only (preserves stability) words[:] = [x for _, x in pairs] # ["a", "pie", "apple", "zebra"] - replaced list with original elements in new order

In other words: sort as if each element were key(element).

  • Use words.sort(key=len) when writing code.
  • Use the pairs version only when explaining how key works.

Common key Conventions

key accepts any callable that takes one argument and returns a comparable value. Common patterns:

TypeKeyUsageOutputUse case
Built-inlenwords.sort(key=len)["a", "pie", "apple", "zebra"]Sort by length
strnums.sort(key=str)[1, 10, 2] (lexicographic)Sort by string representation
intvals.sort(key=int)["1", "2", "10"]Convert before comparing
absnums.sort(key=abs)[-1, 2, -3]Sort by absolute value
Unbound methodstr.lowerlabels.sort(key=str.lower)["alpha", "Beta", "Gamma"]Case-insensitive string sort
str.stripitems.sort(key=str.strip)By stripped valueSort by stripped string
operatoritemgetter(1)pairs.sort(key=itemgetter(1))By second element of each tupleSort by index (from operator)
attrgetter('name')people.sort(key=attrgetter('name'))By .name attributeSort by attribute (from operator)
Lambdalambda x: x[1]pairs.sort(key=lambda x: x[1])By second elementSort by second element
lambda x: (x[0], -x[1])items.sort(key=lambda x: (x[0], -x[1]))Asc first, desc secondMulti-criteria sort
Customfnitems.sort(key=fn)By return value of fn(x)Any custom comparison

Rule: Pass the function reference, not a call; use key=len, not key=len(x). Python calls it once per element.

List vs Other Collections

Python has other built-in types for sequences and collections. Choosing the right one depends on order, mutability, and uniqueness.

TypeOrdered?Mutable?Duplicates?Typical use
listYesYesYesOrdered sequences you change (append, insert, remove)
tupleYesNoYesFixed sequences (e.g. return values, dict keys)
setNoYesNoUnique items, membership tests, set math
dictYes (insertion)Yes (values)No (keys)Key–value mapping; keys unique

Use a list when you need order and the ability to add, remove, or reorder elements. Use a tuple when the sequence should not change. Use a set when you care only about membership or uniqueness. Use a dict when each element is a key–value pair.

Tricky Behaviors

Access - index(x) when missing

lst.index(x) raises ValueError if x is not in the list. Check with x in lst first, or wrap in try/except, or use something like next((i for i, v in enumerate(lst) if v == x), None) for a safe “index or None.”

Add - lst + other with non-list

Concatenation requires both sides to be lists. lst + (1, 2) raises TypeError. Use lst.extend((1, 2)) or lst + list((1, 2)) when the right-hand side is an iterable that is not a list.

Add - extend(d) with a dict

Iterating over a dict yields its keys only. lst.extend(d) appends the keys, not the values or key–value pairs. Use lst.extend(d.values()) or lst.extend(d.items()) if that is what you need.

Loop - Modifying while iterating

Removing or inserting elements in a list while you loop over it can skip elements or raise errors. Prefer building a new list (e.g. comprehension) or iterating over a copy (e.g. for x in lst.copy():) if you mutate inside the loop.

Copy - Assignment is not copy

b = a makes b refer to the same list as a. Changes via a or b affect both. Use b = a.copy() (or list(a), a[:]) when you need an independent copy.

Copy - Shallow copy and nesting

a.copy() copies only the top level. If the list contains inner lists (or other mutables), those are shared. Use copy.deepcopy(a) when you need full independence.

Sort - sort() returns None

lst.sort() mutates in place and returns None. Writing result = lst.sort() leaves result as None. Use result = sorted(lst) when you need a new sorted list.

Mutable default

def f(x=[]) reuses the same list for every call. Use def f(x=None) and if x is None: x = [] so each call gets a fresh list.

Interview Questions

What is the time complexity of list append, insert, and index lookup?

Append O(1) amortized; insert O(n); index lookup O(1); in O(n). List is a dynamic array.

How do you safely get the index of an item when it might not be in the list?

Check with x in lst first, then lst.index(x). Or use try/except ValueError. For a single expression that returns the index or a sentinel: next((i for i, v in enumerate(lst) if v == x), None).

How do you unpack “first” and “rest” of a list?

Use first, *rest = lst. The starred name collects the remaining elements into a list. For “first, middle, last” use first, *mid, last = lst.

When to use + vs extend()?

Use + when you want a new list; both operands must be lists. Use extend() when you want to grow the list in place; it accepts any iterable and mutates.

What does lst.extend(d) do when d is a dict?

Iterating over a dict yields its keys. So lst.extend(d) appends the dict’s keys to the list, not the values or key–value pairs. Use lst.extend(d.values()) or lst.extend(d.items()) for values or pairs.

lst = [...] vs lst[:] = [...] - what is the difference?

lst = [...] rebinds the variable to a new list; other references still see the old list. lst[:] = [...] replaces the contents of the existing list in place; all references see the updated list.

remove() vs pop() - when to use each?

remove(x) removes the first occurrence by value; raises ValueError if missing; returns None. pop(i) removes by index (default last); returns the removed element; raises IndexError if out of range. Use pop() when you need the value or know the index.

What is the difference between lst.clear() and del lst?

lst.clear() empties the list in place; the list object still exists (as []) and any other references to it see the empty list. del lst removes the name lst; the list object may be garbage-collected if nothing else refers to it. Use clear() when you want an empty list but keep the same object; use del when you want to remove the variable.

What is the difference between lst[::-1], list(reversed(lst)), and lst.reverse()?

lst[::-1] and list(reversed(lst)) both produce a new list in reverse order; the original is unchanged. lst.reverse() reverses in place and returns None. Use the first two when you need to keep the original; use reverse() when you can mutate the list.

Why use index-based reverse loop instead of reversed()?

When you need to modify elements by index (swap, overwrite, remove), you must have the index. reversed() and lst[::-1] give values, not indices. The index-based pattern also avoids index shifting when removing in place and translates to other languages.

When does shallow vs deep copy matter for lists?

Shallow copy is fine for flat lists or immutable elements. Use deep copy when the list contains nested mutable structures (lists, dicts) and you need to modify the copy without affecting the original.

sort() vs sorted() - in place vs new list?

sort() mutates the list in place and returns None. sorted() returns a new list and leaves the original unchanged. Use sort() when you can mutate; use sorted() when you need to keep the original.

How does the key parameter work in sort()?

For each element x, Python computes key(x) and sorts by comparing those values. Pass the function reference: key=len, not key=len(x).

What is the difference between lst.sort(reverse=True) and lst.reverse()?

sort(reverse=True) sorts the list in descending order by value. reverse() only reverses the current order; it does not compare values. For [3, 1, 2], sort(reverse=True) gives [3, 2, 1]; reverse() gives [2, 1, 3]. If the list is already sorted ascending, both yield the same sequence.

When would you use sorted() instead of sort()?

Use sorted() when you must keep the original list unchanged, when the source is not a list (e.g. tuple, range), or when you need the sorted result inside an expression. sort() only works on lists and mutates in place.

When would you choose a list over a tuple or a set?

Use a list when you need order and mutability (append, insert, remove, reorder). Use a tuple when the sequence is fixed (e.g. return values, dict keys). Use a set when you care about uniqueness or fast membership and do not need order. Lists allow duplicates and preserve insertion order; sets do not.

Why avoid def f(x=[]) for a default list argument?

The default [] is created once when the function is defined, so every call that omits x shares the same list. Mutations in one call affect later calls. Use def f(x=None) and if x is None: x = [] so each call gets a new list.

Last updated on