Chapter 4 of 14

Python Lists

Master Python lists — creation, indexing, slicing, methods, iteration, comprehensions, nested lists, and common patterns.

Meritshot28 min read
PythonListsArraysData Structures
All Python Chapters

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, not 1.
  • 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 [[]] * n for 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() and deepcopy below.

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: append adds the argument as a single element. extend iterates 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

MethodDescriptionReturns
append(x)Add x to the endNone
insert(i, x)Insert x at index iNone
extend(iterable)Add all items from iterableNone
remove(x)Remove first occurrence of xNone (raises ValueError if missing)
pop(i=-1)Remove and return item at index iThe removed item
clear()Remove all itemsNone
index(x, start, end)Return index of first occurrence of xint (raises ValueError if missing)
count(x)Count occurrences of xint
sort(key, reverse)Sort the list in placeNone
reverse()Reverse the list in placeNone
copy()Return a shallow copylist

Built-in functions that work with lists:

FunctionDescriptionExample
len(lst)Number of elementslen([1,2,3]) returns 3
min(lst)Smallest elementmin([3,1,2]) returns 1
max(lst)Largest elementmax([3,1,2]) returns 3
sum(lst)Sum of all elementssum([1,2,3]) returns 6
sorted(lst)New sorted listsorted([3,1,2]) returns [1,2,3]
reversed(lst)Reverse iteratorlist(reversed([1,2,3])) returns [3,2,1]
any(lst)True if any element is truthyany([0, False, 1]) returns True
all(lst)True if all elements are truthyall([1, True, "hi"]) returns True
enumerate(lst)Iterator of (index, item) pairsSee iteration section
zip(a, b)Pair elements from two listsSee 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, use itertools.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 for clauses 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

Featurelisttuplesetstr
Syntax[1, 2, 3](1, 2, 3){1, 2, 3}"abc"
OrderedYesYesNoYes
MutableYesNoYesNo
DuplicatesAllowedAllowedNot allowedAllowed
Indexinglst[0]tup[0]Not supporteds[0]
Slicinglst[1:3]tup[1:3]Not supporteds[1:3]
HashableNoYesNoYes
Use as dict keyNoYesNoYes
in operator speedO(n)O(n)O(1)O(n)
Best forGeneral mutable sequencesFixed data, dict keysUnique items, fast lookupText

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.

OperationTime ComplexityNotes
lst[i]O(1)Direct index access is instant
lst.append(x)O(1) amortisedVery 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 lstO(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
  • Slicingstart:stop:step, omitting boundaries, negative step for reversing, slice assignment
  • Modifying listsappend(), insert(), extend(), remove(), pop(), del, clear()
  • Sortingsort() (in-place) vs sorted() (new list), custom keys, reverse order
  • Reversingreverse(), reversed(), [::-1] and when to use each
  • Copyingcopy() for shallow copies, deepcopy() when nested mutable objects are involved
  • All major list methods with a reference table
  • Iteration techniquesfor, 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]*m aliasing 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.