Python Lists: Indexing, Slicing, Sorting, Methods, Nested Lists, List Comprehensions, and Memory Behavior
Lists are the workhorse of Python. If you write Python for more than 5 minutes, you will use a list. They hold ordered collections of anything — numbers, strings, other lists, even mixed types. They grow and shrink dynamically. They are the default “I need a collection of things” data structure.
But lists have behaviors that trip up beginners and experienced developers alike. Appending inside a loop vs list comprehension (10x speed difference). Shallow copy vs deep copy (mutating one “copy” changes the “original”). Sorting in place vs returning a new list (the return value trap). This post covers all of it.
Think of a list like a train. Each car (element) has a position number (index). You can add cars to the end (append), insert cars in the middle (insert), remove specific cars (remove/pop), rearrange the order (sort), or detach a section of cars (slicing). The train can carry any type of cargo — passengers, freight, livestock — all in the same train (mixed types).
Table of Contents
- Creating Lists
- Empty Lists and Single-Element Lists
- list() Constructor
- Lists in Memory
- Lists Store References, Not Values
- Mutability — Changing Lists In Place
- Indexing
- Positive and Negative Indexing
- Slicing
- Basic Slicing [start:stop]
- Slicing with Step [start:stop:step]
- Slice Assignment
- Adding Elements
- append() — Add to End
- insert() — Add at Position
- extend() — Add Multiple Elements
- append vs extend
- Removing Elements
- remove() — By Value
- pop() — By Index (Returns the Element)
- del — By Index or Slice
- clear() — Remove All
- Searching and Counting
- index() — Find Position
- count() — Count Occurrences
- in Operator — Check Existence
- Sorting
- sort() — In Place (Modifies the List)
- sorted() — Returns a New List
- Sorting with key Parameter
- reverse() and reversed()
- Copying Lists
- Assignment Is NOT a Copy
- Shallow Copy
- Deep Copy
- When to Use Each
- List Comprehensions
- Basic Comprehension
- Comprehension with Condition
- Nested Comprehension
- Comprehension vs Loop Performance
- Nested Lists (2D Lists)
- Creating and Accessing
- The Nested List Trap
- Iterating Over Lists
- for Loop
- enumerate() — Index + Value
- zip() — Parallel Iteration
- Useful List Operations
- len(), min(), max(), sum()
- any() and all()
- Unpacking
- Lists vs Tuples vs Sets
- Common Mistakes
- Interview Questions
- Wrapping Up
Creating Lists
Empty Lists and Single-Element Lists
# Square brackets []
fruits = ["apple", "banana", "cherry"]
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True, None, [1, 2]] # Any type, including nested lists
# Empty list
empty = []
also_empty = list()
# Single element — no comma needed (unlike tuples!)
single = [42]
# From a range
nums = list(range(10)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
evens = list(range(0, 20, 2)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
list() Constructor
# Convert other iterables to lists
print(list("hello")) # ['h', 'e', 'l', 'l', 'o']
print(list((1, 2, 3))) # [1, 2, 3] — from tuple
print(list({1, 2, 3})) # [1, 2, 3] — from set (order may vary)
print(list({"a": 1, "b": 2})) # ['a', 'b'] — keys only from dict
print(list(range(5))) # [0, 1, 2, 3, 4]
print(list(enumerate("abc"))) # [(0, 'a'), (1, 'b'), (2, 'c')]
Lists in Memory
Lists Store References, Not Values
A list does NOT store values inside itself. It stores references (pointers) to objects on the heap — just like variables. The list object is an array of memory addresses.
my_list = ["apple", "banana", 42]
HEAP MEMORY:
list object (addr: 0xA000)
┌────────────────────────────────┐
│ Internal array of references: │
│ [0] → 0xB100 ──────────────────→ str object "apple"
│ [1] → 0xB200 ──────────────────→ str object "banana"
│ [2] → 0xB300 ──────────────────→ int object 42
│ length: 3 │
│ allocated: 4 (room to grow) │
└────────────────────────────────┘
The list is an array of POINTERS. Each pointer leads to the actual object.
This is why lists can hold mixed types — each slot is just a pointer.
Mutability — Changing Lists In Place
# Lists are MUTABLE — modifications happen on the SAME object
fruits = ["apple", "banana", "cherry"]
print(id(fruits)) # 0xA000
fruits.append("date")
print(id(fruits)) # 0xA000 — SAME address! Object modified in place
fruits[0] = "apricot"
print(id(fruits)) # 0xA000 — still the same object!
# Compare with strings (immutable):
name = "hello"
print(id(name)) # 0xB000
name = name + " world"
print(id(name)) # 0xC000 — DIFFERENT address! New object created
Indexing
Positive and Negative Indexing
List: ["apple", "banana", "cherry", "date", "elderberry"]
Positive: 0 1 2 3 4
Negative: -5 -4 -3 -2 -1
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
print(fruits[0]) # "apple" — first element
print(fruits[4]) # "elderberry" — last element
print(fruits[-1]) # "elderberry" — last element (negative index)
print(fruits[-2]) # "date" — second to last
# Modify by index
fruits[1] = "blueberry"
print(fruits) # ["apple", "blueberry", "cherry", "date", "elderberry"]
# IndexError
# print(fruits[10]) # IndexError: list index out of range
Slicing
Basic Slicing [start:stop]
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[2:5]) # [2, 3, 4] — index 2, 3, 4 (stop is EXCLUSIVE)
print(nums[:4]) # [0, 1, 2, 3] — from start to index 3
print(nums[6:]) # [6, 7, 8, 9] — from index 6 to end
print(nums[:]) # [0, 1, 2, ..., 9] — full copy (new list object!)
print(nums[-3:]) # [7, 8, 9] — last 3 elements
print(nums[:-2]) # [0, 1, 2, 3, 4, 5, 6, 7] — all except last 2
# Slicing NEVER raises IndexError — out-of-range is handled gracefully
print(nums[50:100]) # [] — empty list, no error
Slicing with Step [start:stop:step]
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[::2]) # [0, 2, 4, 6, 8] — every 2nd element
print(nums[1::2]) # [1, 3, 5, 7, 9] — every 2nd, starting at index 1
print(nums[::3]) # [0, 3, 6, 9] — every 3rd element
print(nums[::-1]) # [9, 8, 7, ..., 0] — REVERSED list
print(nums[::-2]) # [9, 7, 5, 3, 1] — reversed, every 2nd
Slice Assignment
# You can REPLACE a slice of a list with new values
nums = [0, 1, 2, 3, 4, 5]
nums[2:4] = [20, 30] # Replace index 2 and 3
print(nums) # [0, 1, 20, 30, 4, 5]
# Replace with DIFFERENT number of elements (list grows or shrinks)
nums[2:4] = [20, 30, 40, 50] # Replaced 2 elements with 4
print(nums) # [0, 1, 20, 30, 40, 50, 4, 5]
# Delete via slice assignment
nums[2:6] = [] # Remove elements at index 2-5
print(nums) # [0, 1, 4, 5]
# Insert via slice assignment (replace nothing)
nums[2:2] = [100, 200] # Insert at index 2 without removing anything
print(nums) # [0, 1, 100, 200, 4, 5]
Adding Elements
append() — Add to End
fruits = ["apple", "banana"]
fruits.append("cherry")
print(fruits) # ["apple", "banana", "cherry"]
# append adds ONE item — even if it is a list
fruits.append(["date", "elderberry"])
print(fruits) # ["apple", "banana", "cherry", ["date", "elderberry"]]
# ↑ nested list! not flat
insert() — Add at Position
fruits = ["apple", "cherry", "date"]
fruits.insert(1, "banana") # Insert at index 1
print(fruits) # ["apple", "banana", "cherry", "date"]
fruits.insert(0, "avocado") # Insert at the beginning
print(fruits) # ["avocado", "apple", "banana", "cherry", "date"]
# insert at len(list) = same as append
fruits.insert(len(fruits), "elderberry") # Same as append
extend() — Add Multiple Elements
fruits = ["apple", "banana"]
fruits.extend(["cherry", "date", "elderberry"])
print(fruits) # ["apple", "banana", "cherry", "date", "elderberry"]
# extend works with ANY iterable
fruits.extend("fig") # Extends with individual characters!
print(fruits) # [..., "f", "i", "g"]
# += is shorthand for extend
fruits += ["grape", "honeydew"]
print(fruits) # [..., "grape", "honeydew"]
append vs extend
| Method | Input | Result |
|---|---|---|
fruits.append(["a", "b"]) |
One item (a list) | [..., ["a", "b"]] — nested |
fruits.extend(["a", "b"]) |
Each item individually | [..., "a", "b"] — flat |
Real-life analogy: append is like putting a bag of groceries into your shopping cart (one item: the bag). extend is like emptying the bag into your cart (each grocery item added individually).
Removing Elements
remove() — By Value
fruits = ["apple", "banana", "cherry", "banana"]
fruits.remove("banana") # Removes FIRST occurrence only
print(fruits) # ["apple", "cherry", "banana"]
# fruits.remove("grape") # ValueError: list.remove(x): x not in list
# Safe removal:
if "grape" in fruits:
fruits.remove("grape")
pop() — By Index (Returns the Element)
fruits = ["apple", "banana", "cherry", "date"]
last = fruits.pop() # Remove and return LAST element
print(last) # "date"
print(fruits) # ["apple", "banana", "cherry"]
second = fruits.pop(1) # Remove and return element at index 1
print(second) # "banana"
print(fruits) # ["apple", "cherry"]
# pop() is the only removal method that RETURNS the removed element
# Use it when you need the value you are removing (e.g., stack operations)
del — By Index or Slice
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
del fruits[0] # Remove first element
print(fruits) # ["banana", "cherry", "date", "elderberry"]
del fruits[1:3] # Remove a slice (index 1 and 2)
print(fruits) # ["banana", "elderberry"]
del fruits # Delete the entire variable (not just contents)
# print(fruits) # NameError: name 'fruits' is not defined
clear() — Remove All
fruits = ["apple", "banana", "cherry"]
fruits.clear() # Remove all elements, keep the list object
print(fruits) # []
print(id(fruits)) # Same address — same object, just empty
Searching and Counting
index() — Find Position
fruits = ["apple", "banana", "cherry", "banana"]
print(fruits.index("banana")) # 1 (first occurrence)
print(fruits.index("banana", 2)) # 3 (search starting from index 2)
# fruits.index("grape") # ValueError if not found
# Safe pattern:
if "grape" in fruits:
pos = fruits.index("grape")
count() — Count Occurrences
nums = [1, 2, 3, 2, 1, 2, 4, 2]
print(nums.count(2)) # 4
print(nums.count(5)) # 0 (not found = 0, no error)
in Operator — Check Existence
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits) # True
print("grape" in fruits) # False
print("grape" not in fruits) # True
# "in" scans the entire list — O(n) time
# For frequent lookups, use a SET instead — O(1) time
fruit_set = set(fruits)
print("banana" in fruit_set) # True — but much faster for large collections
Sorting
sort() — In Place (Modifies the List)
nums = [3, 1, 4, 1, 5, 9, 2, 6]
nums.sort()
print(nums) # [1, 1, 2, 3, 4, 5, 6, 9]
nums.sort(reverse=True)
print(nums) # [9, 6, 5, 4, 3, 2, 1, 1]
# CRITICAL: sort() returns None, NOT the sorted list!
result = nums.sort()
print(result) # None ← THE TRAP!
# This is a VERY common bug:
# sorted_list = my_list.sort() ← sorted_list is None!
# Use sorted() if you need the return value
sorted() — Returns a New List
nums = [3, 1, 4, 1, 5, 9, 2, 6]
sorted_nums = sorted(nums)
print(sorted_nums) # [1, 1, 2, 3, 4, 5, 6, 9]
print(nums) # [3, 1, 4, 1, 5, 9, 2, 6] — original unchanged!
# sorted() works on ANY iterable (tuples, sets, strings, generators)
print(sorted("python")) # ['h', 'n', 'o', 'p', 't', 'y']
print(sorted({3, 1, 2})) # [1, 2, 3]
print(sorted("banana", reverse=True)) # ['n', 'n', 'b', 'a', 'a', 'a']
| Feature | list.sort() |
sorted(iterable) |
|---|---|---|
| Modifies original? | Yes (in place) | No (returns new list) |
| Returns | None | New sorted list |
| Works on | Lists only | Any iterable |
| Memory | No extra memory | Creates a new list |
| Use when | You do not need the original order | You need both original and sorted |
Sorting with key Parameter
# Sort by custom criteria using key=function
words = ["banana", "apple", "Cherry", "date"]
# Sort by length
print(sorted(words, key=len)) # ['date', 'apple', 'banana', 'Cherry']
# Sort case-insensitively
print(sorted(words, key=str.lower)) # ['apple', 'banana', 'Cherry', 'date']
# Sort list of tuples by second element
students = [("Alice", 88), ("Bob", 72), ("Charlie", 95), ("Diana", 85)]
print(sorted(students, key=lambda s: s[1])) # By grade ascending
print(sorted(students, key=lambda s: s[1], reverse=True)) # By grade descending
# Sort list of dicts by a key
employees = [
{"name": "Naveen", "age": 30},
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 35},
]
print(sorted(employees, key=lambda e: e["age"])) # Sorted by age
# Sort by multiple criteria (age ascending, then name alphabetically)
from operator import itemgetter
print(sorted(employees, key=itemgetter("age", "name")))
reverse() and reversed()
# reverse() — modifies in place, returns None
nums = [1, 2, 3, 4, 5]
nums.reverse()
print(nums) # [5, 4, 3, 2, 1]
# reversed() — returns an iterator (does NOT create a new list)
nums = [1, 2, 3, 4, 5]
for n in reversed(nums):
print(n) # 5, 4, 3, 2, 1
print(nums) # [1, 2, 3, 4, 5] — original unchanged
# Convert to list if needed
rev_list = list(reversed(nums)) # [5, 4, 3, 2, 1]
# Or use slicing
rev_list = nums[::-1] # [5, 4, 3, 2, 1] — also a new list
Copying Lists
Assignment Is NOT a Copy
# This is the NUMBER ONE list trap for beginners
original = [1, 2, 3]
copy = original # NOT a copy! Both names point to the SAME list
copy.append(4)
print(original) # [1, 2, 3, 4] ← original changed!
print(copy) # [1, 2, 3, 4]
print(original is copy) # True — same object
Real-life analogy: copy = original is like giving a second key to the same locker. Both keys open the same locker. Putting something in through one key means the other key finds it too.
Shallow Copy
# Three ways to make a shallow copy
original = [1, 2, 3]
copy1 = original.copy() # Method 1: .copy()
copy2 = original[:] # Method 2: slice
copy3 = list(original) # Method 3: list() constructor
copy1.append(4)
print(original) # [1, 2, 3] — unaffected!
print(copy1) # [1, 2, 3, 4]
print(original is copy1) # False — different objects
# BUT shallow copy only copies the TOP level
# Nested objects are still SHARED
nested = [[1, 2], [3, 4]]
shallow = nested.copy()
shallow[0].append(99)
print(nested) # [[1, 2, 99], [3, 4]] ← nested list was shared!
print(shallow) # [[1, 2, 99], [3, 4]]
# Why? The shallow copy created a new outer list, but the inner lists
# are the SAME objects — both the original and copy point to them
Shallow copy of [[1,2], [3,4]]:
original ──→ outer list A
[0] ──→ inner list X [1, 2] ← SHARED
[1] ──→ inner list Y [3, 4] ← SHARED
shallow ──→ outer list B (NEW)
[0] ──→ inner list X [1, 2] ← SAME object as original[0]
[1] ──→ inner list Y [3, 4] ← SAME object as original[1]
Modifying inner list X through shallow affects original too!
Deep Copy
import copy
nested = [[1, 2], [3, 4]]
deep = copy.deepcopy(nested)
deep[0].append(99)
print(nested) # [[1, 2], [3, 4]] — unaffected!
print(deep) # [[1, 2, 99], [3, 4]]
# deepcopy recursively copies EVERYTHING — inner lists are new objects
When to Use Each
| Scenario | Method | Why |
|---|---|---|
| List of numbers/strings | Shallow copy (.copy()) |
No nested mutables — shallow is sufficient |
| List of lists (nested) | Deep copy (copy.deepcopy()) |
Inner lists must be independent |
| List of dicts | Deep copy | Dicts are mutable — shallow shares them |
| Read-only (no modification) | No copy needed | If you never modify, sharing is safe |
List Comprehensions
Basic Comprehension
# Syntax: [expression FOR item IN iterable]
# Loop version:
squares = []
for x in range(10):
squares.append(x ** 2)
# Comprehension version (same result, one line):
squares = [x ** 2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# More examples:
names = ["naveen", "alice", "bob"]
upper_names = [name.upper() for name in names] # ['NAVEEN', 'ALICE', 'BOB']
lengths = [len(name) for name in names] # [6, 5, 3]
first_chars = [name[0] for name in names] # ['n', 'a', 'b']
Comprehension with Condition
# [expression FOR item IN iterable IF condition]
# Only even numbers
evens = [x for x in range(20) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# Only long names
long_names = [name for name in names if len(name) > 4]
print(long_names) # ['naveen', 'alice']
# Filter and transform in one expression
upper_long = [name.upper() for name in names if len(name) > 4]
print(upper_long) # ['NAVEEN', 'ALICE']
# if-else in the EXPRESSION (not the filter)
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
print(labels) # ['even', 'odd', 'even', 'odd', 'even']
# Note: if-else goes BEFORE "for" (expression), if-only goes AFTER "for" (filter)
Nested Comprehension
# Flatten a 2D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Create a 2D list (3x3 matrix of zeros)
zeros = [[0 for col in range(3)] for row in range(3)]
print(zeros) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
# Multiplication table
table = [[i * j for j in range(1, 6)] for i in range(1, 6)]
# [[1, 2, 3, 4, 5], [2, 4, 6, 8, 10], [3, 6, 9, 12, 15], ...]
Comprehension vs Loop Performance
# Comprehensions are typically 20-50% faster than equivalent loops
# because they are optimized at the C level in CPython
# SLOW: append in a loop
result = []
for i in range(1000000):
result.append(i * 2)
# FAST: list comprehension
result = [i * 2 for i in range(1000000)]
# RULE: If your loop just builds a list with append, use a comprehension instead
# Exception: if the logic is complex (multiple statements per iteration),
# keep the loop for readability
Nested Lists (2D Lists)
Creating and Accessing
# A list of lists = a 2D table / matrix
matrix = [
[1, 2, 3], # Row 0
[4, 5, 6], # Row 1
[7, 8, 9], # Row 2
]
print(matrix[0]) # [1, 2, 3] — entire row 0
print(matrix[0][1]) # 2 — row 0, column 1
print(matrix[2][2]) # 9 — row 2, column 2
# Modify
matrix[1][1] = 50
print(matrix) # [[1, 2, 3], [4, 50, 6], [7, 8, 9]]
# Iterate
for row in matrix:
for val in row:
print(val, end=" ")
print()
# 1 2 3
# 4 50 6
# 7 8 9
The Nested List Trap
# WRONG way to create a 2D list:
grid = [[0] * 3] * 3
print(grid) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]] — looks correct
grid[0][0] = 99
print(grid) # [[99, 0, 0], [99, 0, 0], [99, 0, 0]] — ALL rows changed!
# Why? [0, 0, 0] * 3 creates THREE references to the SAME inner list
# All three rows point to the same object!
# CORRECT way:
grid = [[0] * 3 for _ in range(3)] # Each row is a NEW list object
grid[0][0] = 99
print(grid) # [[99, 0, 0], [0, 0, 0], [0, 0, 0]] — only row 0 changed
Real-life analogy: [[0]*3] * 3 is like making one photocopy and putting the SAME copy in three folders. Marking one copy marks all of them. The comprehension version is like making three SEPARATE photocopies — marking one does not affect the others.
Iterating Over Lists
for Loop
# Basic iteration
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# DO NOT use index-based iteration when you do not need the index
# BAD: for i in range(len(fruits)): print(fruits[i])
# GOOD: for fruit in fruits: print(fruit)
enumerate() — Index + Value
# When you need BOTH the index and the value
fruits = ["apple", "banana", "cherry"]
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
# 0: apple
# 1: banana
# 2: cherry
# Start from a different number
for num, fruit in enumerate(fruits, start=1):
print(f"{num}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry
zip() — Parallel Iteration
# Iterate over multiple lists simultaneously
names = ["Alice", "Bob", "Charlie"]
scores = [88, 72, 95]
cities = ["Toronto", "London", "Delhi"]
for name, score, city in zip(names, scores, cities):
print(f"{name} scored {score} from {city}")
# Alice scored 88 from Toronto
# Bob scored 72 from London
# Charlie scored 95 from Delhi
# zip stops at the SHORTEST list
short = [1, 2]
long = [10, 20, 30, 40]
print(list(zip(short, long))) # [(1, 10), (2, 20)] — stops at 2 elements
# Use zip_longest for unequal lengths
from itertools import zip_longest
print(list(zip_longest(short, long, fillvalue=0)))
# [(1, 10), (2, 20), (0, 30), (0, 40)]
Useful List Operations
len(), min(), max(), sum()
nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(len(nums)) # 8
print(min(nums)) # 1
print(max(nums)) # 9
print(sum(nums)) # 31
print(sum(nums) / len(nums)) # 3.875 — average
any() and all()
# any() — True if ANY element is truthy
print(any([False, False, True])) # True
print(any([0, 0, 0])) # False
# all() — True if ALL elements are truthy
print(all([True, True, True])) # True
print(all([True, False, True])) # False
# Practical: check if all values pass a condition
ages = [25, 30, 17, 22]
print(all(age >= 18 for age in ages)) # False (17 is underage)
print(any(age >= 18 for age in ages)) # True (some are 18+)
# Check if any file is a CSV
files = ["data.csv", "report.pdf", "image.png"]
print(any(f.endswith(".csv") for f in files)) # True
Unpacking
# Unpack a list into variables
first, second, third = [1, 2, 3]
print(first, second, third) # 1 2 3
# * captures remaining elements
first, *rest = [1, 2, 3, 4, 5]
print(first) # 1
print(rest) # [2, 3, 4, 5]
*beginning, last = [1, 2, 3, 4, 5]
print(beginning) # [1, 2, 3, 4]
print(last) # 5
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
# Swap using unpacking
a, b = 1, 2
a, b = b, a # a=2, b=1
Lists vs Tuples vs Sets
| Feature | List | Tuple | Set |
|---|---|---|---|
| Syntax | [1, 2, 3] | (1, 2, 3) | {1, 2, 3} |
| Mutable? | Yes | No | Yes |
| Ordered? | Yes | Yes | No |
| Duplicates? | Allowed | Allowed | No duplicates |
| Indexing? | Yes | Yes | No |
| Dict key? | No | Yes | No |
| Lookup speed | O(n) | O(n) | O(1) |
| Use when | Ordered, changeable collection | Fixed data, dict keys, safety | Unique items, fast lookup |
Common Mistakes
- Confusing
sort()andsorted()—sort()modifies the list in place and returnsNone.result = my_list.sort()sets result to None. Usesorted()if you need the return value. - Thinking assignment copies a list —
b = amakes both names point to the SAME list. Usea.copy()ora[:]for a shallow copy, orcopy.deepcopy(a)for nested lists. - Creating 2D lists with
[[0]*n]*m— all rows point to the same inner list. Use[[0]*n for _ in range(m)]instead. - Modifying a list while iterating —
for item in my_list: my_list.remove(item)skips elements because the list changes during iteration. Iterate over a copy:for item in my_list[:]:. - Using
append()when you meanextend()—append([1,2])adds a nested list.extend([1,2])adds 1 and 2 as separate elements. - Using index-based loops when unnecessary —
for i in range(len(list)): print(list[i])is unpythonic. Usefor item in list:orfor i, item in enumerate(list):. - Using a mutable default argument —
def f(items=[])shares the same list across all calls. Usedef f(items=None): items = items or [].
Interview Questions
Q: What is the difference between append() and extend()?
A: append(x) adds x as a single element to the end. If x is a list, it becomes a nested element. extend(iterable) adds each element of the iterable individually. [1,2].append([3,4]) gives [1, 2, [3, 4]]. [1,2].extend([3,4]) gives [1, 2, 3, 4].
Q: What is the difference between sort() and sorted()?
A: sort() modifies the list in place and returns None. sorted() returns a new sorted list and leaves the original unchanged. sorted() works on any iterable (tuples, sets, strings), while sort() only works on lists.
Q: What is the difference between a shallow copy and a deep copy of a list?
A: A shallow copy (.copy()) creates a new outer list but inner objects are still shared references. Modifying a nested list in the copy affects the original. A deep copy (copy.deepcopy()) recursively copies all nested objects, making everything independent. Use deep copy when the list contains mutable elements like other lists or dicts.
Q: Why is [[0]*3]*3 dangerous?
A: The outer *3 creates three references to the SAME inner list. Changing grid[0][0] changes all rows because they are the same object. Use [[0]*3 for _ in range(3)] instead — the comprehension creates a NEW inner list for each row.
Q: How are list comprehensions different from regular loops? A: List comprehensions are 20-50% faster because they are optimized at the C level in CPython. They are also more readable for simple transformations and filtering. However, for complex multi-step logic, regular loops are more readable. Comprehensions should be used when the loop body is a single expression that builds a list.
Q: When would you use a list vs a tuple vs a set?
A: Use a list when you need an ordered, modifiable collection. Use a tuple for fixed data that should not change (function return values, dict keys, data integrity). Use a set when you need unique elements and fast O(1) membership testing. If you find yourself writing if x in my_list frequently, consider converting to a set.
Wrapping Up
Lists are Python’s most versatile data structure — ordered, mutable, and able to hold any type. Master the core operations (append, extend, sort, slice, comprehensions), understand the copy trap (assignment is not a copy), and know when to use alternatives (tuples for immutability, sets for uniqueness and fast lookups). These fundamentals carry directly into PySpark where you will work with distributed lists (RDDs and DataFrames) at massive scale.
Previous in this series: Strings — Slicing, f-Strings, and String Methods
Next in this series: Tuples, Sets, and Frozensets
Naveen Vuppula is a Senior Data Engineering Consultant and app developer based in Ontario, Canada. He writes about Python, SQL, AWS, Azure, and everything data engineering at DriveDataScience.com.