What is a List?
A list is Python's most versatile built-in data structure. It is an ordered, mutable collection that can hold items of any type — integers, strings, floats, booleans, other lists, or even a mix of all of these.
Key characteristics of a Python list:
- Ordered — items maintain their insertion order and can be accessed by position.
- Mutable — you can add, remove, or change items after the list is created.
- Heterogeneous — a single list can hold items of different types.
- Zero-indexed — the first element is at index
0, not1. - Dynamic — lists grow and shrink automatically; you never need to declare a size.
- Allows duplicates — the same value can appear more than once.
fruits = ["apple", "banana", "cherry"]
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", True, 3.14, None]
empty = []
print(type(fruits)) # <class 'list'>
print(len(numbers)) # 5
Lists vs Arrays in Other Languages
If you come from C, Java, or JavaScript, Python lists may surprise you. In those languages an array is typically fixed-size and holds elements of a single type. Python lists are closer to Java's ArrayList or JavaScript's Array — they resize automatically and accept any type. Python does have a dedicated array module for type-restricted, memory-efficient arrays, but for everyday programming lists are the standard choice.
Creating Lists
There are several ways to create a list in Python.
1. Literal Syntax with Square Brackets
# Most common — use square brackets
colors = ["red", "green", "blue"]
scores = [90, 85, 72, 95]
2. The list() Constructor
# Convert a string to a list of characters
chars = list("Python")
print(chars) # ['P', 'y', 't', 'h', 'o', 'n']
# Convert a tuple to a list
coords = list((10, 20, 30))
print(coords) # [10, 20, 30]
# Convert a set to a list (order not guaranteed from the set)
unique = list({3, 1, 2})
print(unique) # [1, 2, 3] (may vary)
3. Using range() to Generate Numeric Lists
# Numbers 0 through 9
digits = list(range(10))
print(digits) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Even numbers from 2 to 20
evens = list(range(2, 21, 2))
print(evens) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# Countdown
countdown = list(range(5, 0, -1))
print(countdown) # [5, 4, 3, 2, 1]
4. Empty List and Single-Element List
# Two ways to create an empty list
empty_a = []
empty_b = list()
print(empty_a == empty_b) # True
# Single-element list — note the trailing comma is optional but can aid clarity
single = [42]
print(len(single)) # 1
5. Repeating Elements
# Create a list of five zeros
zeros = [0] * 5
print(zeros) # [0, 0, 0, 0, 0]
# Initialize a boolean list
flags = [False] * 3
print(flags) # [False, False, False]
Caution: Avoid
[[]] * nfor creating a list of lists — see the Nested Lists section below for why this causes problems.
Accessing Elements
Positive Indexing
Lists are zero-indexed. The first item is at index 0, the second at 1, and so on.
languages = ["Python", "Java", "C++", "JavaScript", "Go"]
print(languages[0]) # Python
print(languages[1]) # Java
print(languages[4]) # Go
Negative Indexing
Negative indices count backward from the end. -1 is the last item, -2 the second-last, and so on.
languages = ["Python", "Java", "C++", "JavaScript", "Go"]
print(languages[-1]) # Go (last)
print(languages[-2]) # JavaScript (second-last)
print(languages[-5]) # Python (same as index 0)
This is extremely useful when you need the last few elements and don't know (or don't want to calculate) the list length.
IndexError
Accessing an index that doesn't exist raises an IndexError:
colors = ["red", "green", "blue"]
# print(colors[5]) # IndexError: list index out of range
# print(colors[-4]) # IndexError: list index out of range
# Safe access pattern — check length first
index = 5
if index < len(colors):
print(colors[index])
else:
print(f"Index {index} is out of range for a list of length {len(colors)}")
Slicing In Depth
Slicing extracts a portion (sub-list) from a list. The syntax is:
list[start:stop:step]
- start — index where the slice begins (inclusive, default
0) - stop — index where the slice ends (exclusive, default end of list)
- step — how many positions to move between elements (default
1)
Basic Slicing
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[2:5]) # [2, 3, 4] — index 2 up to (not including) 5
print(nums[0:3]) # [0, 1, 2] — first three elements
print(nums[7:10]) # [7, 8, 9] — last three elements
Omitting Start or Stop
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[:4]) # [0, 1, 2, 3] — from beginning to index 4
print(nums[6:]) # [6, 7, 8, 9] — from index 6 to end
print(nums[:]) # [0, 1, 2, ... 9] — full copy of the list
Using a Step
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[::2]) # [0, 2, 4, 6, 8] — every second element
print(nums[1::2]) # [1, 3, 5, 7, 9] — every second, starting at index 1
print(nums[::3]) # [0, 3, 6, 9] — every third element
Negative Step (Reversing)
A negative step moves backward through the list.
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] — full reverse
print(nums[::-2]) # [9, 7, 5, 3, 1] — every second element, reversed
print(nums[7:2:-1]) # [7, 6, 5, 4, 3] — from index 7 down to index 3
Slicing Creates a New List (Shallow Copy)
A slice always returns a new list. Modifying the slice does not affect the original.
original = [1, 2, 3, 4, 5]
sliced = original[1:4]
sliced[0] = 99
print(sliced) # [99, 3, 4]
print(original) # [1, 2, 3, 4, 5] — unchanged
Note: This is a shallow copy. If the list contains mutable objects (like nested lists), the inner objects are still shared. See the section on
copy()anddeepcopybelow.
Assigning to a Slice
You can replace a section of a list by assigning to a slice:
letters = ["a", "b", "c", "d", "e"]
letters[1:4] = ["B", "C", "D"]
print(letters) # ['a', 'B', 'C', 'D', 'e']
# The replacement can be a different length
letters[1:4] = ["X"]
print(letters) # ['a', 'X', 'e']
# Insert without removing (empty slice)
letters[1:1] = ["Y", "Z"]
print(letters) # ['a', 'Y', 'Z', 'X', 'e']
# Delete via slice assignment
letters[1:3] = []
print(letters) # ['a', 'X', 'e']
Modifying Lists
Because lists are mutable, Python provides many ways to change them in place.
Changing an Element
fruits = ["apple", "banana", "cherry"]
fruits[1] = "mango"
print(fruits) # ['apple', 'mango', 'cherry']
append() — Add to the End
fruits = ["apple", "banana"]
fruits.append("cherry")
print(fruits) # ['apple', 'banana', 'cherry']
# append adds ONE item — even if it's a list
fruits.append(["date", "elderberry"])
print(fruits) # ['apple', 'banana', 'cherry', ['date', 'elderberry']]
insert() — Add at a Specific Position
fruits = ["apple", "cherry"]
fruits.insert(1, "banana") # insert "banana" at index 1
print(fruits) # ['apple', 'banana', 'cherry']
fruits.insert(0, "avocado") # insert at the beginning
print(fruits) # ['avocado', 'apple', 'banana', 'cherry']
extend() — Add Multiple Items
fruits = ["apple", "banana"]
fruits.extend(["cherry", "date"])
print(fruits) # ['apple', 'banana', 'cherry', 'date']
# You can extend with any iterable
fruits.extend(("elderberry",)) # tuple
fruits.extend("FG") # string — adds each character
print(fruits) # ['apple', 'banana', 'cherry', 'date', 'elderberry', 'F', 'G']
append vs extend:
appendadds the argument as a single element.extenditerates over the argument and adds each item individually. This is one of the most common sources of confusion for beginners.
remove() — Remove by Value
colors = ["red", "green", "blue", "green"]
colors.remove("green") # removes the FIRST occurrence only
print(colors) # ['red', 'blue', 'green']
# Raises ValueError if the item is not found
# colors.remove("yellow") # ValueError: list.remove(x): x not in list
# Safe removal
if "yellow" in colors:
colors.remove("yellow")
pop() — Remove by Index and Return
stack = [10, 20, 30, 40, 50]
last = stack.pop() # removes and returns the last item
print(last) # 50
print(stack) # [10, 20, 30, 40]
second = stack.pop(1) # removes and returns item at index 1
print(second) # 20
print(stack) # [10, 30, 40]
del — Remove by Index or Slice
nums = [0, 1, 2, 3, 4, 5]
del nums[0] # remove first element
print(nums) # [1, 2, 3, 4, 5]
del nums[1:3] # remove a slice
print(nums) # [1, 4, 5]
del nums[:] # remove all elements (list still exists but is empty)
print(nums) # []
# del nums # this would delete the variable entirely
clear() — Empty the List
items = [1, 2, 3]
items.clear()
print(items) # []
print(type(items)) # <class 'list'> — still a list, just empty
sort() vs sorted()
sort() modifies the list in place and returns None. sorted() returns a new sorted list and leaves the original unchanged.
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
# In-place sort (returns None)
result = numbers.sort()
print(numbers) # [1, 1, 2, 3, 4, 5, 6, 9]
print(result) # None — a common mistake is to assign sort() to a variable
# sorted() returns a NEW list
original = [3, 1, 4, 1, 5, 9, 2, 6]
new_sorted = sorted(original)
print(new_sorted) # [1, 1, 2, 3, 4, 5, 6, 9]
print(original) # [3, 1, 4, 1, 5, 9, 2, 6] — unchanged
# Descending order
numbers = [3, 1, 4, 1, 5]
numbers.sort(reverse=True)
print(numbers) # [5, 4, 3, 1, 1]
# Sort with a key function
words = ["banana", "apple", "Cherry", "date"]
words.sort(key=str.lower) # case-insensitive sort
print(words) # ['apple', 'banana', 'Cherry', 'date']
# Sort by length
words.sort(key=len)
print(words) # ['date', 'apple', 'banana', 'Cherry']
# Sort a list of tuples by the second element
students = [("Alice", 88), ("Bob", 72), ("Charlie", 95)]
students.sort(key=lambda s: s[1], reverse=True)
print(students) # [('Charlie', 95), ('Alice', 88), ('Bob', 72)]
reverse() vs reversed() vs [::-1]
nums = [1, 2, 3, 4, 5]
# reverse() — in place, returns None
nums.reverse()
print(nums) # [5, 4, 3, 2, 1]
# reversed() — returns an iterator (lazy), does not modify original
nums = [1, 2, 3, 4, 5]
rev_iter = reversed(nums)
print(list(rev_iter)) # [5, 4, 3, 2, 1]
print(nums) # [1, 2, 3, 4, 5] — unchanged
# [::-1] — returns a new reversed list
nums = [1, 2, 3, 4, 5]
rev_copy = nums[::-1]
print(rev_copy) # [5, 4, 3, 2, 1]
print(nums) # [1, 2, 3, 4, 5] — unchanged
copy() — Shallow Copy
original = [1, 2, 3]
# Three ways to make a shallow copy
copy_a = original.copy()
copy_b = list(original)
copy_c = original[:]
copy_a[0] = 99
print(original) # [1, 2, 3] — unaffected
# SHALLOW copy means nested mutable objects are shared
nested = [[1, 2], [3, 4]]
shallow = nested.copy()
shallow[0][0] = 99
print(nested) # [[99, 2], [3, 4]] — inner list was shared!
When to Use deepcopy
If your list contains nested mutable objects (lists, dicts, etc.) and you need a fully independent copy, use copy.deepcopy():
import copy
nested = [[1, 2], [3, 4]]
deep = copy.deepcopy(nested)
deep[0][0] = 99
print(nested) # [[1, 2], [3, 4]] — completely independent
print(deep) # [[99, 2], [3, 4]]
List Methods Reference Table
| Method | Description | Returns |
|---|---|---|
append(x) | Add x to the end | None |
insert(i, x) | Insert x at index i | None |
extend(iterable) | Add all items from iterable | None |
remove(x) | Remove first occurrence of x | None (raises ValueError if missing) |
pop(i=-1) | Remove and return item at index i | The removed item |
clear() | Remove all items | None |
index(x, start, end) | Return index of first occurrence of x | int (raises ValueError if missing) |
count(x) | Count occurrences of x | int |
sort(key, reverse) | Sort the list in place | None |
reverse() | Reverse the list in place | None |
copy() | Return a shallow copy | list |
Built-in functions that work with lists:
| Function | Description | Example |
|---|---|---|
len(lst) | Number of elements | len([1,2,3]) returns 3 |
min(lst) | Smallest element | min([3,1,2]) returns 1 |
max(lst) | Largest element | max([3,1,2]) returns 3 |
sum(lst) | Sum of all elements | sum([1,2,3]) returns 6 |
sorted(lst) | New sorted list | sorted([3,1,2]) returns [1,2,3] |
reversed(lst) | Reverse iterator | list(reversed([1,2,3])) returns [3,2,1] |
any(lst) | True if any element is truthy | any([0, False, 1]) returns True |
all(lst) | True if all elements are truthy | all([1, True, "hi"]) returns True |
enumerate(lst) | Iterator of (index, item) pairs | See iteration section |
zip(a, b) | Pair elements from two lists | See iteration section |
Iterating Over Lists
Basic for Loop
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# Output:
# apple
# banana
# cherry
enumerate() — Loop with Index
When you need both the index and the value, use enumerate() instead of manually tracking an index counter.
languages = ["Python", "Java", "C++", "Go"]
for index, lang in enumerate(languages):
print(f"{index}: {lang}")
# Output:
# 0: Python
# 1: Java
# 2: C++
# 3: Go
# Start counting from 1
for rank, lang in enumerate(languages, start=1):
print(f"#{rank} {lang}")
# Output:
# #1 Python
# #2 Java
# #3 C++
# #4 Go
zip() — Loop Over Multiple Lists in Parallel
names = ["Alice", "Bob", "Charlie"]
scores = [88, 95, 72]
grades = ["B+", "A", "C"]
for name, score, grade in zip(names, scores, grades):
print(f"{name}: {score} ({grade})")
# Output:
# Alice: 88 (B+)
# Bob: 95 (A)
# Charlie: 72 (C)
Note:
zip()stops at the shortest list. If the lists have different lengths, useitertools.zip_longest()to iterate until the longest is exhausted.
while Loop with Index
Sometimes you need manual index control — for example, when modifying the list during iteration or skipping elements dynamically.
nums = [10, 20, 30, 40, 50]
i = 0
while i < len(nums):
print(f"Index {i}: {nums[i]}")
i += 1
Iterating in Reverse
colors = ["red", "green", "blue"]
# Using reversed()
for color in reversed(colors):
print(color)
# Using negative step slice
for color in colors[::-1]:
print(color)
# Using range with negative step
for i in range(len(colors) - 1, -1, -1):
print(f"{i}: {colors[i]}")
List Comprehensions
List comprehensions are one of Python's most powerful features. They provide a concise, readable way to create new lists by transforming or filtering existing iterables.
Basic Comprehension
# Syntax: [expression for item in iterable]
# Squares of 1 through 10
squares = [x ** 2 for x in range(1, 11)]
print(squares) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# Convert temperatures from Celsius to Fahrenheit
celsius = [0, 10, 20, 30, 40]
fahrenheit = [(c * 9/5) + 32 for c in celsius]
print(fahrenheit) # [32.0, 50.0, 68.0, 86.0, 104.0]
# Extract first letter of each word
words = ["Python", "is", "awesome"]
initials = [w[0] for w in words]
print(initials) # ['P', 'i', 'a']
Comprehension with Condition (Filter)
# Syntax: [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]
# Words longer than 3 characters
words = ["I", "love", "Python", "programming", "is", "fun"]
long_words = [w for w in words if len(w) > 3]
print(long_words) # ['love', 'Python', 'programming']
# Positive numbers only
numbers = [-5, 3, -1, 7, -2, 8, 0]
positives = [n for n in numbers if n > 0]
print(positives) # [3, 7, 8]
Comprehension with if-else (Transform)
When you need to choose between two expressions, place the if-else before the for — not after it.
# Syntax: [expr_if_true if condition else expr_if_false for item in iterable]
# Label numbers as even or odd
labels = ["even" if x % 2 == 0 else "odd" for x in range(6)]
print(labels) # ['even', 'odd', 'even', 'odd', 'even', 'odd']
# Clamp values to a range [0, 100]
raw_scores = [105, -3, 87, 150, 42, -10, 99]
clamped = [max(0, min(100, s)) for s in raw_scores]
print(clamped) # [100, 0, 87, 100, 42, 0, 99]
# Replace negatives with zero
data = [4, -2, 7, -5, 3]
cleaned = [x if x >= 0 else 0 for x in data]
print(cleaned) # [4, 0, 7, 0, 3]
Nested Comprehensions
You can nest for clauses. This is especially useful for flattening matrices or generating combinations.
# Flatten a 2D list (matrix) into a 1D 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]
# This is equivalent to:
flat = []
for row in matrix:
for num in row:
flat.append(num)
# Generate all coordinate pairs
coords = [(x, y) for x in range(3) for y in range(3)]
print(coords)
# [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)]
Performance: Comprehension vs Regular Loop
List comprehensions are generally faster than equivalent for loops with append() because the comprehension is optimised internally by the Python interpreter.
import time
n = 1_000_000
# Regular loop
start = time.time()
result_loop = []
for i in range(n):
result_loop.append(i ** 2)
loop_time = time.time() - start
# List comprehension
start = time.time()
result_comp = [i ** 2 for i in range(n)]
comp_time = time.time() - start
print(f"Loop: {loop_time:.4f}s")
print(f"Comprehension: {comp_time:.4f}s")
# Comprehension is typically 20-30% faster
Readability rule: If a comprehension becomes hard to read (e.g., more than two
forclauses or complex conditions), break it out into a regular loop. Code clarity always wins.
Nested Lists
A list can contain other lists as elements. This is how you represent tables, matrices, grids, and other multi-dimensional data in Python.
Creating a Matrix
# A 3x3 matrix
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Create dynamically with comprehension
rows, cols = 3, 4
grid = [[0 for _ in range(cols)] for _ in range(rows)]
print(grid)
# [[0, 0, 0, 0],
# [0, 0, 0, 0],
# [0, 0, 0, 0]]
Accessing Elements
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Access with matrix[row][col]
print(matrix[0][0]) # 1 — top-left
print(matrix[1][2]) # 6 — row 1, column 2
print(matrix[2][1]) # 8 — row 2, column 1
print(matrix[-1][-1]) # 9 — bottom-right
# Get entire row
print(matrix[1]) # [4, 5, 6]
# Get a column (requires a loop or comprehension)
col_0 = [row[0] for row in matrix]
print(col_0) # [1, 4, 7]
Iterating Over a 2D List
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Print each element with its position
for i, row in enumerate(matrix):
for j, val in enumerate(row):
print(f"matrix[{i}][{j}] = {val}")
# Pretty-print the matrix
for row in matrix:
print(" ".join(str(x).rjust(3) for x in row))
# 1 2 3
# 4 5 6
# 7 8 9
Common Mistake: The Aliasing Trap
This is one of the most frequently encountered bugs for Python beginners:
# WRONG — all three rows are the SAME list object
bad_grid = [[0] * 3] * 3
bad_grid[0][0] = 5
print(bad_grid)
# [[5, 0, 0], [5, 0, 0], [5, 0, 0]] — all rows changed!
# Why? Because [[0]*3] * 3 creates three references to the
# same inner list, not three separate lists.
# CORRECT — use a comprehension to create independent rows
good_grid = [[0] * 3 for _ in range(3)]
good_grid[0][0] = 5
print(good_grid)
# [[5, 0, 0], [0, 0, 0], [0, 0, 0]] — only the first row changed
This happens because the * operator on a list does not deep-copy its elements. Each slot in the outer list points to the same inner list object.
Common List Patterns
Finding Max, Min, Sum, and Average
scores = [78, 92, 85, 63, 97, 88, 71]
highest = max(scores)
lowest = min(scores)
total = sum(scores)
average = sum(scores) / len(scores)
print(f"Highest: {highest}") # 97
print(f"Lowest: {lowest}") # 63
print(f"Total: {total}") # 574
print(f"Average: {average:.1f}") # 82.0
# Index of the max value
best_index = scores.index(max(scores))
print(f"Best score is at index {best_index}") # 4
Counting Occurrences
votes = ["A", "B", "A", "C", "B", "A", "B", "A"]
# Using list.count()
print(votes.count("A")) # 4
print(votes.count("B")) # 3
print(votes.count("C")) # 1
# Count all items at once with a dict comprehension
tally = {item: votes.count(item) for item in set(votes)}
print(tally) # {'A': 4, 'B': 3, 'C': 1}
# For large lists, use collections.Counter (more efficient)
from collections import Counter
tally = Counter(votes)
print(tally) # Counter({'A': 4, 'B': 3, 'C': 1})
print(tally.most_common(1)) # [('A', 4)]
Removing Duplicates
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
# Method 1: Convert to set (does NOT preserve order)
unique_unordered = list(set(data))
print(unique_unordered) # order may vary
# Method 2: Preserve insertion order (Python 3.7+)
unique_ordered = list(dict.fromkeys(data))
print(unique_ordered) # [3, 1, 4, 5, 9, 2, 6]
# Method 3: Manual loop (works everywhere, preserves order)
seen = set()
unique_manual = []
for item in data:
if item not in seen:
seen.add(item)
unique_manual.append(item)
print(unique_manual) # [3, 1, 4, 5, 9, 2, 6]
Flattening Nested Lists
nested = [[1, 2], [3, 4, 5], [6], [7, 8, 9, 10]]
# Using a list comprehension
flat = [item for sublist in nested for item in sublist]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Using itertools.chain (handles large data efficiently)
from itertools import chain
flat = list(chain.from_iterable(nested))
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# For deeply nested structures, use a recursive function
def deep_flatten(lst):
result = []
for item in lst:
if isinstance(item, list):
result.extend(deep_flatten(item))
else:
result.append(item)
return result
deeply_nested = [1, [2, [3, [4, 5]], 6], 7]
print(deep_flatten(deeply_nested)) # [1, 2, 3, 4, 5, 6, 7]
Splitting a List into Chunks
def chunk_list(lst, size):
"""Split a list into chunks of the given size."""
return [lst[i:i + size] for i in range(0, len(lst), size)]
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(chunk_list(data, 3)) # [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
print(chunk_list(data, 4)) # [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]]
print(chunk_list(data, 5)) # [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
Checking if a List is Sorted
def is_sorted(lst, reverse=False):
"""Check if a list is sorted in ascending (or descending) order."""
if reverse:
return all(lst[i] >= lst[i + 1] for i in range(len(lst) - 1))
return all(lst[i] <= lst[i + 1] for i in range(len(lst) - 1))
print(is_sorted([1, 2, 3, 4, 5])) # True
print(is_sorted([1, 3, 2, 4, 5])) # False
print(is_sorted([5, 4, 3, 2, 1], reverse=True)) # True
# One-liner alternative
nums = [1, 2, 3, 4, 5]
print(nums == sorted(nums)) # True — but less efficient for large lists
List vs Other Sequences
| Feature | list | tuple | set | str |
|---|---|---|---|---|
| Syntax | [1, 2, 3] | (1, 2, 3) | {1, 2, 3} | "abc" |
| Ordered | Yes | Yes | No | Yes |
| Mutable | Yes | No | Yes | No |
| Duplicates | Allowed | Allowed | Not allowed | Allowed |
| Indexing | lst[0] | tup[0] | Not supported | s[0] |
| Slicing | lst[1:3] | tup[1:3] | Not supported | s[1:3] |
| Hashable | No | Yes | No | Yes |
| Use as dict key | No | Yes | No | Yes |
in operator speed | O(n) | O(n) | O(1) | O(n) |
| Best for | General mutable sequences | Fixed data, dict keys | Unique items, fast lookup | Text |
When to use which:
- list — when you need an ordered, changeable collection (most common case).
- tuple — when data should not change (function return values, dict keys, coordinates).
- set — when you need uniqueness or fast membership testing.
- str — for text (a sequence of characters).
Performance Notes
Understanding time complexity helps you write efficient code, especially with large datasets.
| Operation | Time Complexity | Notes |
|---|---|---|
lst[i] | O(1) | Direct index access is instant |
lst.append(x) | O(1) amortised | Very fast — the go-to method for adding |
lst.pop() | O(1) | Removing the last element is fast |
lst.pop(0) | O(n) | Removing from the front shifts everything |
lst.insert(0, x) | O(n) | Inserting at the front shifts everything |
lst.insert(i, x) | O(n) | Shifts elements from index i onward |
x in lst | O(n) | Must scan the entire list in the worst case |
lst.sort() | O(n log n) | TimSort algorithm |
lst.copy() | O(n) | Must copy every reference |
lst.reverse() | O(n) | Swaps elements in place |
len(lst) | O(1) | Length is stored internally |
lst.count(x) | O(n) | Scans the full list |
del lst[i] | O(n) | Shifts elements after index i |
Practical Performance Tips
# TIP 1: Use append(), not insert(0, x) — for building lists
# BAD — O(n) per insert, O(n^2) total
result = []
for i in range(10000):
result.insert(0, i)
# GOOD — O(1) per append, O(n) total, then reverse once
result = []
for i in range(10000):
result.append(i)
result.reverse()
# TIP 2: Use a set for frequent membership checks
# BAD — O(n) per lookup
big_list = list(range(100000))
print(99999 in big_list) # slow on large lists
# GOOD — O(1) per lookup
big_set = set(big_list)
print(99999 in big_set) # nearly instant
# TIP 3: Use collections.deque for queue operations
from collections import deque
queue = deque()
queue.append("task1") # add to right — O(1)
queue.append("task2")
queue.appendleft("urgent") # add to left — O(1)
task = queue.popleft() # remove from left — O(1)
print(task) # "urgent"
# With a regular list, popleft/insert(0,x) would be O(n)
Practical Examples
Example 1: Student Grade Analyser
# Analyse student scores and assign grades
students = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
scores = [88, 72, 95, 63, 81]
def get_grade(score):
"""Return a letter grade based on the score."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
# Build a report
print("--- Student Grade Report ---")
print(f"{'Name':<12} {'Score':>5} {'Grade':>5}")
print("-" * 24)
for name, score in zip(students, scores):
grade = get_grade(score)
print(f"{name:<12} {score:>5} {grade:>5}")
print("-" * 24)
print(f"{'Class Average':<12} {sum(scores)/len(scores):>5.1f}")
print(f"{'Highest':<12} {max(scores):>5} ({students[scores.index(max(scores))]})")
print(f"{'Lowest':<12} {min(scores):>5} ({students[scores.index(min(scores))]})")
# Students who scored above average
avg = sum(scores) / len(scores)
above_avg = [name for name, score in zip(students, scores) if score > avg]
print(f"\nAbove average: {', '.join(above_avg)}")
Example 2: Shopping Cart with Quantities
# A simple shopping cart using a list of dictionaries
cart = []
def add_item(name, price, quantity=1):
"""Add an item to the cart or increase its quantity if it already exists."""
for item in cart:
if item["name"] == name:
item["quantity"] += quantity
return
cart.append({"name": name, "price": price, "quantity": quantity})
def remove_item(name):
"""Remove an item from the cart by name."""
global cart
cart = [item for item in cart if item["name"] != name]
def get_total():
"""Calculate the total cost of all items in the cart."""
return sum(item["price"] * item["quantity"] for item in cart)
def display_cart():
"""Print a formatted summary of the cart."""
if not cart:
print("Your cart is empty.")
return
print(f"\n{'Item':<20} {'Price':>8} {'Qty':>4} {'Subtotal':>10}")
print("-" * 44)
for item in cart:
subtotal = item["price"] * item["quantity"]
print(f"{item['name']:<20} {item['price']:>8.2f} {item['quantity']:>4} {subtotal:>10.2f}")
print("-" * 44)
print(f"{'Total':<20} {'':>8} {'':>4} {get_total():>10.2f}")
# Usage
add_item("Python Book", 45.99)
add_item("USB Cable", 9.99, 2)
add_item("Mouse Pad", 12.50)
add_item("USB Cable", 9.99, 1) # adds 1 more USB Cable
display_cart()
# Item Price Qty Subtotal
# --------------------------------------------
# Python Book 45.99 1 45.99
# USB Cable 9.99 3 29.97
# Mouse Pad 12.50 1 12.50
# --------------------------------------------
# Total 88.46
Example 3: Matrix Transpose
# Transpose a matrix (swap rows and columns)
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Method 1: Nested comprehension
transposed = [[row[col] for row in matrix] for col in range(len(matrix[0]))]
print("Original:")
for row in matrix:
print(row)
print("\nTransposed:")
for row in transposed:
print(row)
# Original: Transposed:
# [1, 2, 3] [1, 4, 7]
# [4, 5, 6] [2, 5, 8]
# [7, 8, 9] [3, 6, 9]
# Method 2: Using zip() with unpacking (elegant one-liner)
transposed_zip = [list(row) for row in zip(*matrix)]
print(transposed_zip)
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
# Method 3: Manual nested loop (clearest for beginners)
rows = len(matrix)
cols = len(matrix[0])
transposed_manual = []
for c in range(cols):
new_row = []
for r in range(rows):
new_row.append(matrix[r][c])
transposed_manual.append(new_row)
Practice Exercises
Test your understanding with these exercises. Try to solve each one before looking at the hints.
Exercise 1: Second Largest
Write a function that takes a list of numbers and returns the second largest value without using sort() or sorted().
# Example:
# second_largest([10, 5, 8, 20, 3]) should return 10
Exercise 2: Rotate a List
Write a function rotate(lst, k) that rotates a list k positions to the right. For example, rotating [1, 2, 3, 4, 5] by 2 gives [4, 5, 1, 2, 3].
# Hint: use slicing with len(lst) - k as the split point
Exercise 3: Merge Two Sorted Lists
Write a function that takes two already sorted lists and merges them into a single sorted list without using sort() or sorted().
# Example:
# merge_sorted([1, 3, 5], [2, 4, 6]) should return [1, 2, 3, 4, 5, 6]
Exercise 4: List Intersection Write a function that returns a list of elements common to two lists, preserving the order from the first list and without duplicates.
# Example:
# intersection([1, 2, 2, 3, 4], [2, 3, 5]) should return [2, 3]
Exercise 5: Group by Length Write a function that takes a list of strings and groups them by their length into a dictionary.
# Example:
# group_by_length(["hi", "hey", "hello", "go", "bye"])
# should return {2: ['hi', 'go'], 3: ['hey', 'bye'], 5: ['hello']}
Exercise 6: Pascal's Triangle
Write a function that generates the first n rows of Pascal's Triangle as a list of lists. Each element is the sum of the two elements directly above it.
# Example for n=5:
# [
# [1],
# [1, 1],
# [1, 2, 1],
# [1, 3, 3, 1],
# [1, 4, 6, 4, 1]
# ]
Summary
In this chapter, you learned:
- What lists are — ordered, mutable, heterogeneous, zero-indexed collections
- Creating lists — literal syntax,
list(),range(), repetition with* - Accessing elements — positive indexing, negative indexing, handling
IndexError - Slicing —
start:stop:step, omitting boundaries, negative step for reversing, slice assignment - Modifying lists —
append(),insert(),extend(),remove(),pop(),del,clear() - Sorting —
sort()(in-place) vssorted()(new list), custom keys, reverse order - Reversing —
reverse(),reversed(),[::-1]and when to use each - Copying —
copy()for shallow copies,deepcopy()when nested mutable objects are involved - All major list methods with a reference table
- Iteration techniques —
for,enumerate(),zip(),while, and reverse iteration - List comprehensions — basic, filtered, if-else, nested, and performance benefits
- Nested lists — creating matrices, accessing elements, and avoiding the
[[0]*n]*maliasing trap - Common patterns — max/min/average, counting, deduplication, flattening, chunking, sorted checks
- Comparison with other sequences — when to choose list vs tuple vs set vs string
- Performance characteristics — O(1) vs O(n) operations and practical optimisation tips
- Practical examples — grade analyser, shopping cart, matrix transpose
Next up: Strings — deep dive into string manipulation, formatting, methods, and regular expressions.