Python Lists ā The Complete Guide to Python's Most Versatile Data Structure
š” Lists are the most widely used data structure in Python. This comprehensive guide covers everything from basic creation to advanced techniques, giving you complete mastery over Python's most versatile collection type.
Learning Objectives
By the end of this tutorial, you will be able to:
- Create lists using various methods and understand when to use each
- Use indexing and slicing to access and manipulate list elements
- Apply all built-in list methods confidently
- Write basic and intermediate list comprehensions
- Understand nested lists and 2D data structures
- Recognize performance implications of different operations
- Avoid common pitfalls that trip up Python developers
- Apply lists to solve real-world programming problems
What Are Lists?
A list is an ordered, mutable collection in Python. Think of it as a container that holds multiple items in a specific sequence.
# Lists can contain any type of data ā integers, strings, floats, booleans, even other lists
mixed = [42, "hello", 3.14, True, None, [1, 2, 3]]
print(mixed)
# Output: [42, 'hello', 3.14, True, None, [1, 2, 3]]
Key Characteristics
| Characteristic | Description | Example |
|---|---|---|
| Ordered | Elements maintain their insertion order | [1, 2, 3] is different from [3, 2, 1] |
| Mutable | Elements can be changed after creation | lst[0] = 99 works fine |
| Dynamic | Can grow or shrink at runtime | lst.append(4) adds an element |
| Heterogeneous | Can mix different types | [1, "two", 3.0] is valid |
| Duplicates allowed | Same value can appear multiple times | [1, 1, 2, 3] is valid |
| Zero-indexed | First element is at index 0 | lst[0] gets the first item |
Lists Store References, Not Copies
When you create a list, Python stores references (pointers) to the objects, not the objects themselves:
a = [1, 2, 3]
b = a # b points to the same list object as a
b.append(4)
print(a) # [1, 2, 3, 4] ā a is also changed!
print(a is b) # True ā same object in memory
This is why understanding shallow vs deep copy matters (covered later).
Creating Lists
1. Literal Syntax (Most Common)
# Empty list
empty = []
# List of strings
fruits = ["apple", "banana", "cherry"]
# List of numbers
numbers = [1, 2, 3, 4, 5]
# Mixed types
mixed = [1, "two", 3.0, True]
print(fruits) # ['apple', 'banana', 'cherry']
print(numbers) # [1, 2, 3, 4, 5]
print(mixed) # [1, 'two', 3.0, True]
2. The list() Constructor
# From a string (each character becomes an element)
chars = list("Python")
print(chars) # ['P', 'y', 't', 'h', 'o', 'n']
# From a range
numbers = list(range(1, 6))
print(numbers) # [1, 2, 3, 4, 5]
# From a tuple
from_tuple = list((10, 20, 30))
print(from_tuple) # [10, 20, 30]
# From a set (order not guaranteed)
from_set = list({3, 1, 2})
print(from_set) # [1, 2, 3] (order may vary)
3. List Multiplication (Initialization)
# Create a list with repeated values
zeros = [0] * 5
print(zeros) # [0, 0, 0, 0, 0]
ones = [1] * 3
print(ones) # [1, 1, 1]
# Repeat a pattern
pattern = [0, 1] * 4
print(pattern) # [0, 1, 0, 1, 0, 1, 0, 1]
# WARNING: Be careful with mutable objects in multiplication
nested = [[0]] * 3
nested[0][0] = 99
print(nested) # [[99], [99], [99]] ā All sublists are the same object!
4. List Comprehension (Preview)
# Create a list of squares
squares = [x**2 for x in range(5)]
print(squares) # [0, 1, 4, 9, 16]
# Filter even numbers
evens = [x for x in range(10) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
# Transform strings
upper = [word.upper() for word in ["hello", "world"]]
print(upper) # ['HELLO', 'WORLD']
5. Using dict() and zip()
# Create list of tuples from two lists
names = ["Alice", "Bob"]
scores = [95, 87]
paired = list(zip(names, scores))
print(paired) # [('Alice', 95), ('Bob', 87)]
# Create list from dictionary
prices = {"apple": 1.20, "banana": 0.50}
items = list(prices.keys())
print(items) # ['apple', 'banana']
Indexing and Slicing
Lists use the same indexing and slicing mechanics as strings.
Positive Indexing
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
# 0 1 2 3 4
print(fruits[0]) # apple
print(fruits[2]) # cherry
print(fruits[-1]) # elderberry (last element)
print(fruits[-2]) # date (second to last)
Slicing
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:5]) # [2, 3, 4] ā from index 2 to 4
print(numbers[:4]) # [0, 1, 2, 3] ā first 4 elements
print(numbers[6:]) # [6, 7, 8, 9] ā from index 6 to end
print(numbers[::2]) # [0, 2, 4, 6, 8] ā every other element
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] ā reversed
print(numbers[1:8:3]) # [1, 4, 7] ā every 3rd from index 1 to 7
Slice Assignment (Mutating Slices)
This is where lists differ from strings ā you can assign to slices:
letters = ['a', 'b', 'c', 'd', 'e']
# Replace a slice
letters[1:3] = ['B', 'C', 'D'] # Replaces 2 elements with 3
print(letters) # ['a', 'B', 'C', 'D', 'd', 'e']
# Insert using slice assignment
letters[1:1] = ['X', 'Y'] # Insert before index 1
print(letters) # ['a', 'X', 'Y', 'B', 'C', 'D', 'd', 'e']
# Delete using slice assignment
letters[2:5] = [] # Remove elements at indices 2, 3, 4
print(letters) # ['a', 'X', 'B', 'd', 'e']
# Replace entire list content
letters[:] = [1, 2, 3]
print(letters) # [1, 2, 3]
Copying with Slicing
original = [1, 2, 3, 4, 5]
# This creates a shallow copy
copy = original[:]
copy[0] = 99
print(original) # [1, 2, 3, 4, 5] ā unchanged
print(copy) # [99, 2, 3, 4, 5] ā modified
# Equivalent to list.copy()
copy2 = original.copy()
Modifying Individual Elements
scores = [85, 92, 78, 95, 88]
scores[2] = 100 # Change the 78 to 100
print(scores) # [85, 92, 100, 95, 88]
scores[-1] = 0 # Change the last element
print(scores) # [85, 92, 100, 95, 0]
List Methods (Comprehensive)
Adding Elements
| Method | Description | Time Complexity |
|---|---|---|
append(x) | Add x to the end | O(1) amortized |
extend(iterable) | Add all items from iterable | O(k) where k is length of iterable |
insert(i, x) | Insert x at position i | O(n) |
# append() ā adds a single element to the end
fruits = ["apple", "banana"]
fruits.append("cherry")
print(fruits) # ['apple', 'banana', 'cherry']
# You can append any object, including lists (as a single element)
fruits.append(["date", "elderberry"])
print(fruits) # ['apple', 'banana', 'cherry', ['date', 'elderberry']]
print(len(fruits)) # 4 ā the nested list counts as one element
# extend() ā adds multiple elements from an iterable
fruits = ["apple", "banana"]
fruits.extend(["cherry", "date"])
print(fruits) # ['apple', 'banana', 'cherry', 'date']
# extend vs append ā key difference
a = [1, 2]
a.extend([3, 4]) # Adds 3 and 4 as separate elements
print(a) # [1, 2, 3, 4]
b = [1, 2]
b.append([3, 4]) # Adds [3, 4] as a single element
print(b) # [1, 2, [3, 4]]
# insert() ā adds element at a specific position
names = ["Alice", "Bob", "David"]
names.insert(2, "Charlie") # Insert "Charlie" at index 2
print(names) # ['Alice', 'Bob', 'Charlie', 'David']
names.insert(0, "Zara") # Insert at the beginning
print(names) # ['Zara', 'Alice', 'Bob', 'Charlie', 'David']
Removing Elements
| Method | Description | Time Complexity |
|---|---|---|
remove(x) | Remove first occurrence of x | O(n) |
pop(i) | Remove and return element at index i | O(n) for iā -1, O(1) for i=-1 |
del lst[i] | Remove element at index i | O(n) |
clear() | Remove all elements | O(n) |
# remove() ā removes first occurrence of a value
colors = ["red", "blue", "green", "blue"]
colors.remove("blue")
print(colors) # ['red', 'green', 'blue'] ā only first "blue" removed
# Raises ValueError if value not found
try:
colors.remove("yellow")
except ValueError as e:
print(f"Error: {e}")
# pop() ā removes and returns element at index
stack = [10, 20, 30, 40, 50]
last = stack.pop()
print(last) # 50
print(stack) # [10, 20, 30, 40]
second = stack.pop(1)
print(second) # 20
print(stack) # [10, 30, 40]
# pop() on empty list raises IndexError
try:
[].pop()
except IndexError as e:
print(f"Error: {e}")
# del ā keyword for removing elements
data = [10, 20, 30, 40, 50]
del data[2] # Remove element at index 2
print(data) # [10, 20, 40, 50]
del data[1:3] # Remove a slice
print(data) # [10, 50]
del data[:] # Clear the list (same as data.clear())
print(data) # []
# clear() ā remove all elements
items = [1, 2, 3, 4, 5]
items.clear()
print(items) # []
Searching
| Method | Description | Time Complexity |
|---|---|---|
index(x) | Return index of first occurrence of x | O(n) |
index(x, i, j) | Find x between indices i and j | O(j-i) |
count(x) | Count occurrences of x | O(n) |
x in lst | Check if x is in the list | O(n) |
# index() ā find position of an element
primes = [2, 3, 5, 7, 11, 13, 17]
print(primes.index(7)) # 3
print(primes.index(13)) # 5
# index() with start and stop parameters
numbers = [1, 2, 3, 2, 1]
print(numbers.index(2, 2)) # Find 2 starting from index 2 ā 3
print(numbers.index(1, 1, 4)) # Find 1 between index 1 and 3 ā 4? No, let's check
# Actually this would raise ValueError since 1 is not between indices 1-3
# Let me correct:
print(numbers.index(1, 0, 4)) # Find 1 between index 0 and 3 ā 0
# count() ā how many times does a value appear?
data = [1, 2, 2, 3, 2, 4, 2]
print(data.count(2)) # 4
print(data.count(5)) # 0 (not found)
# The 'in' operator ā membership testing
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits) # True
print("grape" in fruits) # False
print("grape" not in fruits) # True
Sorting
| Method/Function | Description | Returns |
|---|---|---|
lst.sort() | Sort list in-place | None (modifies original) |
sorted(lst) | Return new sorted list | New list |
lst.sort(key=fn) | Sort with custom key | None |
lst.sort(reverse=True) | Sort in descending order | None |
reversed(lst) | Return reverse iterator | Iterator (not a list) |
lst.reverse() | Reverse in-place | None |
# sort() ā in-place sorting (modifies the original list)
scores = [85, 92, 78, 95, 88]
scores.sort()
print(scores) # [78, 85, 88, 92, 95]
scores.sort(reverse=True)
print(scores) # [95, 92, 88, 85, 78]
# sorted() ā returns a new sorted list (original unchanged)
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
new_sorted = sorted(numbers)
print(numbers) # [3, 1, 4, 1, 5, 9, 2, 6] ā original unchanged
print(new_sorted) # [1, 1, 2, 3, 4, 5, 6, 9]
# Sorting with a key function
words = ["banana", "apple", "cherry", "date"]
words.sort(key=len) # Sort by string length
print(words) # ['date', 'apple', 'banana', 'cherry']
# Case-insensitive sorting
names = ["charlie", "Alice", "bob"]
names.sort(key=str.lower)
print(names) # ['Alice', 'bob', 'charlie']
# Sort by a specific attribute or index
students = [("Alice", 85), ("Bob", 92), ("Charlie", 78)]
students.sort(key=lambda student: student[1]) # Sort by score
print(students) # [('Charlie', 78), ('Alice', 85), ('Bob', 92)]
# reversed() ā returns an iterator, not a list
nums = [1, 2, 3]
rev = reversed(nums)
print(list(rev)) # [3, 2, 1] ā need to convert to list
print(nums) # [1, 2, 3] ā original unchanged
# reverse() ā in-place reversal
nums = [1, 2, 3]
nums.reverse()
print(nums) # [3, 2, 1]
Other Methods
| Method | Description | Time Complexity |
|---|---|---|
copy() | Return a shallow copy | O(n) |
len(lst) | Return number of elements | O(1) |
sum(lst) | Sum all numeric elements | O(n) |
min(lst) | Return smallest element | O(n) |
max(lst) | Return largest element | O(n) |
# copy() ā create a shallow copy
original = [1, 2, 3, [4, 5]]
copied = original.copy()
copied[0] = 99
print(original) # [1, 2, 3, [4, 5]] ā unchanged
# But shallow copy has caveats with nested objects
copied[3][0] = 99
print(original) # [1, 2, 3, [99, 5]] ā nested list is shared!
# len(), sum(), min(), max()
numbers = [4, 2, 7, 1, 9, 3]
print(len(numbers)) # 6
print(sum(numbers)) # 26
print(min(numbers)) # 1
print(max(numbers)) # 9
# With strings
words = ["apple", "banana", "cherry"]
print(min(words)) # 'apple' (lexicographic minimum)
print(max(words)) # 'cherry' (lexicographic maximum)
List Comprehensions (Preview)
List comprehensions provide a concise way to create lists. A detailed tutorial is available in the list comprehensions module, but here's what you need to know:
Basic Syntax
# [expression for item in iterable]
squares = [x**2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# Equivalent loop:
squares = []
for x in range(10):
squares.append(x**2)
Filtering with Conditions
# [expression for item in iterable if condition]
evens = [x for x in range(20) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# Filter and transform
long_words = [word.upper() for word in ["hi", "hello", "hey", "howdy"] if len(word) > 2]
print(long_words) # ['HELLO', 'HOWDY']
Nested Comprehensions
# 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 3x3 identity matrix
identity = [[1 if i == j else 0 for j in range(3)] for i in range(3)]
print(identity) # [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
Conditional Expressions (Ternary)
# [expr_if_true if condition else expr_if_false for item in iterable]
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
print(labels) # ['even', 'odd', 'even', 'odd', 'even']
Nested Lists
Nested lists (lists within lists) are commonly used for matrices, tables, and structured data.
2D Lists / Matrices
# Create a 3x3 matrix
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Access individual elements
print(matrix[0][0]) # 1 (row 0, column 0)
print(matrix[1][2]) # 6 (row 1, column 2)
print(matrix[2][1]) # 8 (row 2, column 1)
# Access entire rows
print(matrix[0]) # [1, 2, 3] (first row)
print(matrix[-1]) # [7, 8, 9] (last row)
# Modify elements
matrix[1][1] = 99
print(matrix) # [[1, 2, 3], [4, 99, 6], [7, 8, 9]]
# Iterate through a 2D list
for row in matrix:
for element in row:
print(element, end=" ")
print()
# Output:
# 1 2 3
# 4 99 6
# 7 8 9
Matrix Operations
# Matrix addition
A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]
C = [[A[i][j] + B[i][j] for j in range(2)] for i in range(2)]
print(C) # [[6, 8], [10, 12]]
# Transpose a matrix
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed) # [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
Deep Copy vs Shallow Copy
This is critical when working with nested lists:
import copy
original = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Shallow copy (using list methods)
shallow = original.copy()
shallow[0][0] = 99
print(original[0][0]) # 99 ā original is affected!
# Deep copy (using copy module)
original = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original[0][0]) # 1 ā original is NOT affected
| Copy Type | What It Does | Use When |
|---|---|---|
copy() / [:] / list() | Shallow copy ā copies references | Top-level elements are immutable |
copy.deepcopy() | Deep copy ā recursively copies everything | Nested mutable objects |
Lists vs Other Sequences
Lists vs Tuples
| Feature | List [] | Tuple () |
|---|---|---|
| Mutable | Yes | No |
| Syntax | [1, 2, 3] | (1, 2, 3) |
| Performance | Slightly slower | Slightly faster |
| Use case | Collection of similar items that changes | Fixed record of heterogeneous data |
| Dictionary key | No (unhashable) | Yes (if all elements are hashable) |
# Use a list when data changes
shopping_cart = ["milk", "eggs", "bread"]
shopping_cart.append("butter")
# Use a tuple when data is fixed
rgb_color = (255, 128, 0) # RGB values won't change
# Tuples can be dictionary keys; lists cannot
locations = {
(40.7128, -74.0060): "New York",
(51.5074, -0.1278): "London"
}
# This would raise TypeError:
# locations[[40.7128, -74.0060]] = "New York"
Lists vs NumPy Arrays
| Feature | List | NumPy Array |
|---|---|---|
| Element types | Can be mixed | Must be same type |
| Math operations | Element-wise not built-in | Vectorized operations |
| Memory | More memory efficient for small data | More efficient for large numerical data |
| Speed | Slower for computations | Much faster for computations |
# Lists ā element-wise operations require loops
a = [1, 2, 3]
b = [4, 5, 6]
c = [a[i] + b[i] for i in range(len(a))] # [5, 7, 9]
# NumPy ā vectorized operations
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
c = a + b # array([5, 7, 9])
Lists vs Generators
| Feature | List | Generator |
|---|---|---|
| Memory | Stores all elements | Produces elements on demand |
| Iteration | Can iterate multiple times | Single pass only |
| Indexing | Supports lst[i] | No random access |
| Use case | When you need the full data | When you only need to iterate once |
# List ā all elements stored in memory
squares_list = [x**2 for x in range(1_000_000)] # ~8MB of memory
# Generator ā produces values on demand
squares_gen = (x**2 for x in range(1_000_000)) # ~0 memory
# Use generators for large datasets
import sys
print(sys.getsizeof(squares_list)) # ~8,448,728 bytes
print(sys.getsizeof(squares_gen)) # ~208 bytes
Common Patterns
Stacking / Unpacking
# Unpacking a list
first, second, *rest = [1, 2, 3, 4, 5]
print(first) # 1
print(second) # 2
print(rest) # [3, 4, 5]
# Unpacking with ignore
first, _, third, *_ = [1, 2, 3, 4, 5]
print(first) # 1
print(third) # 3
# Swapping variables using unpacking
a, b = 10, 20
a, b = b, a
print(a, b) # 20 10
# Unpacking in function arguments
def greet(name, age):
return f"{name} is {age} years old"
person = ["Alice", 30]
print(greet(*person)) # "Alice is 30 years old"
Rotating Lists
from collections import deque
def rotate_left(lst, k):
"""Rotate list k positions to the left."""
d = deque(lst)
d.rotate(-k)
return list(d)
def rotate_right(lst, k):
"""Rotate list k positions to the right."""
d = deque(lst)
d.rotate(k)
return list(d)
numbers = [1, 2, 3, 4, 5]
print(rotate_left(numbers, 2)) # [3, 4, 5, 1, 2]
print(rotate_right(numbers, 2)) # [4, 5, 1, 2, 3]
Removing Duplicates (Preserving Order)
def remove_duplicates(lst):
"""Remove duplicates while preserving order."""
seen = set()
result = []
for item in lst:
if item not in seen:
seen.add(item)
result.append(item)
return result
data = [3, 1, 2, 3, 1, 4, 2, 5]
print(remove_duplicates(data)) # [3, 1, 2, 4, 5]
# Simpler approach (Python 3.7+ preserves dict insertion order)
unique = list(dict.fromkeys(data))
print(unique) # [3, 1, 2, 4, 5]
Flattening Nested Lists
# Flatten a list of lists
nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = [item for sublist in nested for item in sublist]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Using itertools.chain for better performance
from itertools import chain
flat = list(chain.from_iterable(nested))
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Grouping Elements
from itertools import groupby
data = [1, 1, 2, 2, 2, 3, 3, 3, 3]
for value, group in groupby(sorted(data)):
print(f"{value}: {list(group)}")
# Output:
# 1: [1, 1]
# 2: [2, 2, 2]
# 3: [3, 3, 3, 3]
Performance Considerations
Understanding time complexity helps you write efficient code:
| Operation | Time Complexity | Notes |
|---|---|---|
append(x) | O(1) amortized | Fast ā Python pre-allocates memory |
pop() (from end) | O(1) | Fast ā same as append |
pop(0) (from start) | O(n) | Slow ā all elements shift |
insert(0, x) (at start) | O(n) | Slow ā all elements shift |
x in lst (search) | O(n) | Linear search through list |
lst[i] (index access) | O(1) | Random access is fast |
len(lst) | O(1) | Length is cached |
sort() | O(n log n) | Timsort algorithm |
copy() | O(n) | Must copy all elements |
When to Use collections.deque
from collections import deque
# deque is optimized for append/popleft operations
# Use it when you frequently add/remove from the beginning
# List ā O(n) for insert at start
lst = [1, 2, 3]
lst.insert(0, 0) # Slow for large lists
# deque ā O(1) for appendleft
dq = deque([1, 2, 3])
dq.appendleft(0) # Fast regardless of size
Performance Example
import time
# Bad: Building a list by inserting at the start
start = time.time()
lst = []
for i in range(100_000):
lst.insert(0, i) # O(n) each time ā O(n²) total
print(f"insert(0, x): {time.time() - start:.4f}s")
# Good: Build list and reverse, or use deque
start = time.time()
lst = []
for i in range(100_000):
lst.append(i) # O(1) each time ā O(n) total
lst.reverse()
print(f"append + reverse: {time.time() - start:.4f}s")
# Best: Use deque for this pattern
start = time.time()
dq = deque()
for i in range(100_000):
dq.appendleft(i)
print(f"deque.appendleft: {time.time() - start:.4f}s")
Common Mistakes
1. Mutable Default Arguments
# WRONG ā mutable default is shared across all calls
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] ā unexpected! The default list persists
# CORRECT ā use None as default
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [2] ā works correctly
2. Modifying While Iterating
# WRONG ā skipping elements during iteration
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
if num % 2 == 0:
numbers.remove(num) # Bad! Modifies list while iterating
print(numbers) # [1, 3, 5] ā seems OK but is unreliable
# CORRECT ā create a new list or iterate over a copy
numbers = [1, 2, 3, 4, 5, 6]
numbers = [num for num in numbers if num % 2 != 0]
print(numbers) # [1, 3, 5]
# Alternative: iterate over a copy
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers[:]: # numbers[:] creates a copy
if num % 2 == 0:
numbers.remove(num)
3. Shallow Copy Confusion
# Problem: nested objects are shared in shallow copies
original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0][0] = 99
print(original[0][0]) # 99 ā original is modified!
# Solution: use deep copy for nested structures
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original[0][0]) # 1 ā original is safe
4. == vs is for Lists
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True ā same values
print(a is b) # False ā different objects in memory
print(a is c) # True ā c points to the same object as a
# Use == to compare values
# Use is to check if two variables reference the same object
5. Forgetting sort() Returns None
# WRONG ā sort() returns None, not the sorted list
numbers = [3, 1, 4, 1, 5]
result = numbers.sort()
print(result) # None!
print(numbers) # [1, 1, 3, 4, 5] ā the list is sorted in-place
# CORRECT ā either use sort() for in-place, or sorted() for a new list
numbers = [3, 1, 4, 1, 5]
sorted_numbers = sorted(numbers) # Returns new list
print(sorted_numbers) # [1, 1, 3, 4, 5]
Practice Exercises
Exercise 1: Remove Duplicates from a List
Write a function that removes duplicates from a list while preserving the original order.
def remove_duplicates(lst):
"""Remove duplicates while preserving order."""
seen = set()
result = []
for item in lst:
if item not in seen:
seen.add(item)
result.append(item)
return result
# Test cases
print(remove_duplicates([1, 2, 2, 3, 4, 4, 5])) # [1, 2, 3, 4, 5]
print(remove_duplicates(["a", "b", "a", "c", "b"])) # ['a', 'b', 'c']
print(remove_duplicates([])) # []
print(remove_duplicates([1, 1, 1, 1])) # [1]
See Solution
def remove_duplicates(lst):
"""Remove duplicates while preserving order."""
seen = set()
result = []
for item in lst:
if item not in seen:
seen.add(item)
result.append(item)
return result
# Test cases
print(remove_duplicates([1, 2, 2, 3, 4, 4, 5])) # [1, 2, 3, 4, 5]
print(remove_duplicates(["a", "b", "a", "c", "b"])) # ['a', 'b', 'c']
print(remove_duplicates([])) # []
print(remove_duplicates([1, 1, 1, 1])) # [1]
Exercise 2: Rotate a List
Write a function that rotates a list by k positions to the right. Elements that fall off the end wrap around to the beginning.
def rotate_right(lst, k):
"""Rotate list k positions to the right."""
if not lst:
return lst
k = k % len(lst) # Handle k > len(lst)
return lst[-k:] + lst[:-k]
# Test cases
print(rotate_right([1, 2, 3, 4, 5], 2)) # [4, 5, 1, 2, 3]
print(rotate_right([1, 2, 3, 4, 5], 7)) # [4, 5, 1, 2, 3] (7 % 5 = 2)
print(rotate_right([1, 2, 3, 4, 5], 0)) # [1, 2, 3, 4, 5]
print(rotate_right([], 3)) # []
See Solution
def rotate_right(lst, k):
"""Rotate list k positions to the right."""
if not lst:
return lst
k = k % len(lst) # Handle k > len(lst)
return lst[-k:] + lst[:-k]
# Test cases
print(rotate_right([1, 2, 3, 4, 5], 2)) # [4, 5, 1, 2, 3]
print(rotate_right([1, 2, 3, 4, 5], 7)) # [4, 5, 1, 2, 3] (7 % 5 = 2)
print(rotate_right([1, 2, 3, 4, 5], 0)) # [1, 2, 3, 4, 5]
print(rotate_right([], 3)) # []
Exercise 3: Find Common Elements
Write a function that finds the common elements between two lists (without duplicates in the result).
def common_elements(lst1, lst2):
"""Find common elements between two lists."""
return list(set(lst1) & set(lst2))
# Alternative preserving order
def common_elements_ordered(lst1, lst2):
"""Find common elements preserving order from lst1."""
set2 = set(lst2)
seen = set()
result = []
for item in lst1:
if item in set2 and item not in seen:
seen.add(item)
result.append(item)
return result
# Test cases
print(common_elements([1, 2, 3, 4, 5], [3, 4, 5, 6, 7])) # [3, 4, 5]
print(common_elements(["a", "b", "c"], ["b", "c", "d"])) # ['b', 'c']
print(common_elements([1, 1, 2, 3], [1, 2, 2])) # [1, 2]
See Solution
def common_elements(lst1, lst2):
"""Find common elements between two lists."""
return list(set(lst1) & set(lst2))
# Alternative preserving order
def common_elements_ordered(lst1, lst2):
"""Find common elements preserving order from lst1."""
set2 = set(lst2)
seen = set()
result = []
for item in lst1:
if item in set2 and item not in seen:
seen.add(item)
result.append(item)
return result
# Test cases
print(common_elements([1, 2, 3, 4, 5], [3, 4, 5, 6, 7])) # [3, 4, 5]
print(common_elements(["a", "b", "c"], ["b", "c", "d"])) # ['b', 'c']
print(common_elements([1, 1, 2, 3], [1, 2, 2])) # [1, 2]
Key Takeaways
- Lists are ordered, mutable, and can hold any data type ā they're Python's most versatile collection
- Use literal syntax
[]for creating lists ā it's faster and more Pythonic thanlist() - Understand
append()vsextend()ā append adds one element, extend adds multiple sort()modifies in-place,sorted()returns a new list ā choose based on whether you need to preserve the original- List comprehensions are concise and fast ā use them for simple transformations and filters
- Watch out for shallow copies with nested lists ā use
copy.deepcopy()when needed - Avoid mutable default arguments ā always use
Noneas the default - Consider performance ā
insert(0, x)is O(n), usedequefor frequent start insertions - Use
infor membership testing ā but remember it's O(n) for lists - Lists are not the best choice for everything ā use tuples for fixed data, sets for unique elements, and generators for large datasets
āļø Next Steps: Now that you've mastered lists, move on to the List Comprehensions tutorial to learn advanced comprehension patterns, or explore Tuples to understand immutable sequences.