Creating Lists
Lists are ordered, mutable sequences that can hold any mix of data types. They are defined with square brackets.
# Empty list
empty = []
empty2 = list()
# List of numbers
numbers = [1, 2, 3, 4, 5]
# Mixed types
mixed = [42, "hello", 3.14, True, None]
# List from another iterable
chars = list("Python") # ['P', 'y', 't', 'h', 'o', 'n']
nums = list(range(1, 6)) # [1, 2, 3, 4, 5]
doubled = list(range(0, 20, 2)) # [0, 2, 4, 6, ..., 18]
# Length and membership
fruits = ["apple", "banana", "cherry"]
print(len(fruits)) # 3
print("banana" in fruits) # True
print("grape" not in fruits) # True
# Indexing (0-based, negative from end)
print(fruits[0]) # apple
print(fruits[-1]) # cherry
print(fruits[-2]) # banana
# Lists are mutable - items can be changed
fruits[1] = "blueberry"
print(fruits) # ['apple', 'blueberry', 'cherry']
# Iterating
for fruit in fruits:
print(fruit)
# With index using enumerate()
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
# 0: apple
# 1: blueberry
# 2: cherry
List Methods
items = [3, 1, 4, 1, 5, 9, 2, 6]
# Adding items
items.append(7) # add to end: [..., 6, 7]
items.insert(0, 0) # insert at index 0: [0, 3, 1, ...]
items.extend([8, 9]) # add all from iterable (like += )
# items += [10, 11] # same as extend
# Removing items
items.remove(1) # remove first occurrence of value 1
popped = items.pop() # remove and return last item
popped2 = items.pop(0) # remove and return item at index 0
del items[2] # delete item at index 2 (no return value)
del items[1:3] # delete a slice
# Search
nums = [10, 20, 30, 20, 40]
print(nums.index(20)) # 1 - index of first occurrence
print(nums.count(20)) # 2 - how many times value appears
# Reversing
nums.reverse() # in place: [40, 20, 30, 20, 10]
print(list(reversed(nums))) # returns iterator, convert to list
# Clearing
backup = nums.copy()
nums.clear() # empties the list: []
print(nums) # []
print(backup) # [40, 20, 30, 20, 10] (not affected)
# Concatenation and repetition (create new lists)
a = [1, 2, 3]
b = [4, 5, 6]
c = a + b # [1, 2, 3, 4, 5, 6] (new list)
d = a * 3 # [1, 2, 3, 1, 2, 3, 1, 2, 3]
# Unpacking into variables
first, *rest = [1, 2, 3, 4, 5]
print(first) # 1
print(rest) # [2, 3, 4, 5]
head, *middle, tail = [1, 2, 3, 4, 5]
print(head, middle, tail) # 1 [2, 3, 4] 5
| Method | Description | Returns |
|---|---|---|
append(x) | Add item to end | None |
insert(i, x) | Insert at index i | None |
extend(iter) | Add all items from iterable | None |
remove(x) | Remove first x (ValueError if missing) | None |
pop(i=-1) | Remove and return item at i | item |
index(x) | Index of first x | int |
count(x) | Count occurrences of x | int |
sort() | Sort in place | None |
reverse() | Reverse in place | None |
copy() | Shallow copy | list |
clear() | Remove all items | None |
Slicing
List slicing uses the same [start:stop:step] syntax as strings. Slices always return a new list.
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[2:5]) # [2, 3, 4]
print(nums[:3]) # [0, 1, 2] - first 3
print(nums[-3:]) # [7, 8, 9] - last 3
print(nums[::2]) # [0, 2, 4, 6, 8] - every 2nd
print(nums[::-1]) # [9, 8, 7, ..., 0] - reversed
# Slice assignment - replace a slice
nums[2:5] = [20, 30, 40]
print(nums) # [0, 1, 20, 30, 40, 5, 6, 7, 8, 9]
# Insert without replacing (empty slice assignment)
nums[2:2] = [100, 200]
print(nums) # [0, 1, 100, 200, 20, 30, 40, 5, 6, 7, 8, 9]
# Delete a slice
del nums[2:4]
print(nums) # [0, 1, 20, 30, 40, 5, 6, 7, 8, 9]
# Shallow copy via slice
original = [1, 2, 3, 4, 5]
copy = original[:] # same as original.copy()
copy[0] = 99
print(original) # [1, 2, 3, 4, 5] - not affected
print(copy) # [99, 2, 3, 4, 5]
Sorting Lists
nums = [3, 1, 4, 1, 5, 9, 2, 6]
# sort() modifies in place, returns None
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]
# sorted() returns a new list, original unchanged
original = [3, 1, 4, 1, 5]
new_sorted = sorted(original)
print(original) # [3, 1, 4, 1, 5] - unchanged
print(new_sorted) # [1, 1, 3, 4, 5]
# Sorting strings (lexicographic)
words = ["banana", "apple", "Cherry", "date"]
print(sorted(words)) # ['Cherry', 'apple', 'banana', 'date']
print(sorted(words, key=str.lower)) # ['apple', 'banana', 'Cherry', 'date']
# Sort by key function
people = [
{"name": "Charlie", "age": 35},
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30},
]
# Sort by age
by_age = sorted(people, key=lambda p: p["age"])
for p in by_age:
print(p["name"], p["age"])
# Alice 25
# Bob 30
# Charlie 35
# Sort by multiple keys: primary by age, secondary by name
data = [
("Alice", 30), ("Bob", 25), ("Charlie", 30), ("Dave", 25)
]
# Sort by age (index 1) then name (index 0)
from operator import itemgetter
data.sort(key=itemgetter(1, 0))
print(data)
# [('Bob', 25), ('Dave', 25), ('Alice', 30), ('Charlie', 30)]
# min() and max() with key
print(min(people, key=lambda p: p["age"])) # {'name': 'Alice', 'age': 25}
List Comprehensions
List comprehensions are a concise, readable way to create lists from iterables.
# Basic: [expression for item in iterable]
squares = [x ** 2 for x in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
# With condition: [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]
# Transform strings
words = ["hello", "WORLD", "Python"]
lower = [w.lower() for w in words]
print(lower) # ['hello', 'world', 'python']
# Filter and transform together
nums = [1, -2, 3, -4, 5, -6]
positives_squared = [x**2 for x in nums if x > 0]
print(positives_squared) # [1, 9, 25]
# Conditional expression (ternary) inside comprehension
labels = ["positive" if x > 0 else "negative" for x in nums]
print(labels) # ['positive', 'negative', 'positive', 'negative', 'positive', 'negative']
# 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]
# Equivalent regular loop
flat2 = []
for row in matrix:
for num in row:
flat2.append(num)
# Transpose a matrix
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed) # [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
# Practical: parse CSV line
csv_line = "Alice,30,Engineer,London"
fields = [f.strip() for f in csv_line.split(",")]
print(fields) # ['Alice', '30', 'Engineer', 'London']
Nested Lists
Lists can contain other lists, creating multi-dimensional structures like matrices or tables.
# 2D list (matrix)
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
# Access: matrix[row][col]
print(matrix[0][0]) # 1 - top left
print(matrix[1][1]) # 5 - center
print(matrix[2][-1]) # 9 - bottom right
# Modify
matrix[0][0] = 100
print(matrix[0]) # [100, 2, 3]
# Iterate over rows
for row in matrix:
print(row)
# Iterate over all cells
for row in matrix:
for cell in row:
print(cell, end=" ")
print()
# Create with comprehension
size = 3
grid = [[0] * size for _ in range(size)]
print(grid) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
# WARNING: do NOT use this pattern - all rows are the same object
bad_grid = [[0] * size] * size # wrong!
bad_grid[0][0] = 1
print(bad_grid) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] - all rows changed!
# Use list() to count/sum columns and rows
col_sums = [sum(matrix[r][c] for r in range(3)) for c in range(3)]
row_sums = [sum(row) for row in matrix]
Copying Lists
Python lists store references. Understanding shallow vs deep copy is critical to avoid unexpected mutations.
import copy
# Assignment - both names point to the same list
a = [1, 2, 3]
b = a # b is NOT a copy - it's the same object
b.append(4)
print(a) # [1, 2, 3, 4] - a was also modified!
# Shallow copy - creates new list, but nested objects are still shared
original = [[1, 2], [3, 4]]
shallow = original.copy() # or original[:] or list(original)
shallow.append([5, 6]) # only in shallow, not original
print(original) # [[1, 2], [3, 4]] - OK
shallow[0][0] = 99 # modifies nested list - shared reference!
print(original) # [[99, 2], [3, 4]] - original also changed!
# Deep copy - fully independent, even nested structures
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 99 # modifies deep copy only
print(original) # [[1, 2], [3, 4]] - not affected
# Summary: which to use?
# = assignment -> same object (aliasing), not a copy
# .copy() / [:] -> shallow copy (fine for flat lists)
# copy.deepcopy() -> deep copy (required for nested mutable objects)
# Flat list - shallow copy is safe
nums = [1, 2, 3]
nums_copy = nums.copy()
nums_copy[0] = 99
print(nums) # [1, 2, 3] - not affected (ints are immutable)
For lists that only contain immutable items (ints, strings, tuples), a shallow copy is safe. Only use copy.deepcopy() when the list contains mutable objects (other lists, dicts, custom objects) that you intend to modify independently. Deep copy is slower, so prefer shallow copy by default.