🌱 Beginner

Lists

📖 Lesson 06 ⏱ 30 min 🧪 5 questions 💻 3 exercises

🎯 Learning Objectives

  • Create lists and understand they are ordered, mutable, and allow duplicates
  • Access elements with indexing and extract sublists with slicing
  • Modify lists in place: add, remove, and change elements
  • Use essential list methods: append, extend, insert, pop, remove, sort
  • Iterate over lists with for loops and enumerate()
  • Understand list copying, nested lists, and common patterns

What is a List?

A list is Python's most versatile data structure — an ordered, mutable collection that can hold items of any type (including mixed types).

# Creating lists
numbers = [1, 2, 3, 4, 5]
fruits = ["apple", "banana", "cherry"]
mixed = [42, "hello", 3.14, True, None]
empty = []

# Lists can contain other lists
matrix = [[1, 2], [3, 4], [5, 6]]

# list() constructor
letters = list("Python")   # ['P', 'y', 't', 'h', 'o', 'n']
nums = list(range(5))      # [0, 1, 2, 3, 4]
create_lists.py
Lists are mutable — you can change, add, and remove elements after creation. This is the key difference from strings and tuples, which are immutable.

Indexing & Slicing

Lists use the same indexing and slicing rules as strings (zero-based, negative indices count from the end):

colors = ["red", "green", "blue", "yellow", "purple"]
#           0       1        2       3         4
#          -5      -4       -3      -2        -1

# Indexing
print(colors[0])     # "red"
print(colors[-1])    # "purple"

# Slicing [start:stop:step]
print(colors[1:3])   # ["green", "blue"]
print(colors[:2])    # ["red", "green"]
print(colors[2:])    # ["blue", "yellow", "purple"]
print(colors[::2])   # ["red", "blue", "purple"]
print(colors[::-1])  # reversed list
indexing.py
Slicing a list returns a new list (a shallow copy of that portion). Indexing returns the element itself.

Modifying Lists

Unlike strings, you can change list elements in place:

nums = [10, 20, 30, 40, 50]

# Change a single element
nums[0] = 99
print(nums)  # [99, 20, 30, 40, 50]

# Change a slice (can even change length!)
nums[1:3] = [200, 300, 350]
print(nums)  # [99, 200, 300, 350, 40, 50]

# Delete with del
del nums[0]
print(nums)  # [200, 300, 350, 40, 50]

# Delete a slice
del nums[1:3]
print(nums)  # [200, 40, 50]
modifying.py

Adding Elements

MethodActionReturns
.append(x)Add x to the endNone (modifies in place)
.insert(i, x)Insert x at index iNone
.extend(iterable)Add all items from iterableNone
+ operatorConcatenate two listsNew list
* operatorRepeat a listNew list
fruits = ["apple", "banana"]

# append — one item at the end
fruits.append("cherry")
print(fruits)  # ["apple", "banana", "cherry"]

# insert — at a specific position
fruits.insert(1, "avocado")
print(fruits)  # ["apple", "avocado", "banana", "cherry"]

# extend — merge another list
fruits.extend(["date", "elderberry"])
print(fruits)  # ["apple", "avocado", "banana", "cherry", "date", "elderberry"]

# Concatenation (creates a NEW list)
more = fruits + ["fig", "grape"]

# Repetition
zeros = [0] * 5   # [0, 0, 0, 0, 0]
adding.py
append() vs extend(): .append([1,2]) adds the list as a single element. .extend([1,2]) adds each item individually. This is a common source of bugs.

Removing Elements

MethodActionReturns
.pop(i)Remove & return item at index i (default: last)The removed item
.remove(x)Remove first occurrence of value xNone
.clear()Remove all itemsNone
del list[i]Delete item at index
items = ["a", "b", "c", "d", "e"]

# pop — remove by index, returns the item
last = items.pop()      # "e", items = ["a", "b", "c", "d"]
second = items.pop(1)   # "b", items = ["a", "c", "d"]

# remove — remove by value (first occurrence only)
items.remove("c")       # items = ["a", "d"]

# clear — empty the list
items.clear()           # items = []
removing.py
.remove(x) raises a ValueError if x is not in the list. Check with if x in list: first, or use a try/except.

Sorting & Searching

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]

# Descending order
nums.sort(reverse=True)
print(nums)  # [9, 6, 5, 4, 3, 2, 1, 1]

# sorted() — returns a NEW sorted list (original unchanged)
original = [3, 1, 4, 1, 5]
ordered = sorted(original)
print(original)  # [3, 1, 4, 1, 5] (unchanged)
print(ordered)   # [1, 1, 3, 4, 5]

# Sort strings by length (custom key)
words = ["banana", "pie", "strawberry", "kiwi"]
words.sort(key=len)
print(words)  # ["pie", "kiwi", "banana", "strawberry"]

# reverse() — reverse in place
nums = [1, 2, 3]
nums.reverse()
print(nums)  # [3, 2, 1]
sorting.py
# Searching
fruits = ["apple", "banana", "cherry", "banana"]

print("banana" in fruits)       # True
print(fruits.index("cherry"))   # 2 (first occurrence)
print(fruits.count("banana"))   # 2

# index() raises ValueError if not found — check first!
if "mango" in fruits:
    pos = fruits.index("mango")
searching.py
Use sorted() when you need a sorted copy and want to keep the original. Use .sort() when you're done with the original order — it's slightly faster (no extra memory allocation).

Iterating Over Lists

colors = ["red", "green", "blue"]

# Basic for loop
for color in colors:
    print(color)

# With index — use enumerate()
for i, color in enumerate(colors):
    print(f"{i}: {color}")
# 0: red
# 1: green
# 2: blue

# Start enumerate at a different number
for i, color in enumerate(colors, start=1):
    print(f"{i}. {color}")
# 1. red
# 2. green
# 3. blue
iteration.py
Never iterate by index with range(len(list)) unless you truly need only the index. Use enumerate() when you need both index and value — it's more Pythonic and less error-prone.

Copying Lists

Assigning a list to a new variable does not copy it — both names refer to the same object:

# This is NOT a copy — it's an alias
a = [1, 2, 3]
b = a
b.append(4)
print(a)  # [1, 2, 3, 4] — a was also modified!

# Shallow copy methods (all equivalent)
c = a.copy()
c = a[:]
c = list(a)

c.append(5)
print(a)  # [1, 2, 3, 4] — a is unchanged
print(c)  # [1, 2, 3, 4, 5]

# Deep copy — needed for nested lists
import copy
nested = [[1, 2], [3, 4]]
deep = copy.deepcopy(nested)
deep[0][0] = 99
print(nested)  # [[1, 2], [3, 4]] — original safe
print(deep)    # [[99, 2], [3, 4]]
copying.py
A shallow copy copies the list structure but not nested objects. If your list contains other lists (or any mutable objects), changes to nested items will be shared. Use copy.deepcopy() when nesting is involved.

Useful List Patterns

# Length, min, max, sum
nums = [4, 8, 2, 9, 1]
print(len(nums))   # 5
print(min(nums))   # 1
print(max(nums))   # 9
print(sum(nums))   # 24

# Unpacking
first, *rest = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

a, b, *_ = [10, 20, 30, 40]
print(a, b)   # 10 20

# zip — iterate multiple lists in parallel
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# List comprehension (preview — full coverage in Lesson 16)
squares = [x ** 2 for x in range(1, 6)]
print(squares)  # [1, 4, 9, 16, 25]
patterns.py
🤖

Ask your AI tutor! Confused about shallow vs deep copy? Unsure when to use append vs extend? Lists are foundational — make sure you're solid here before moving on.

💻 Exercises

01 List Statistics

Given a list of numbers [23, 45, 12, 67, 34, 89, 2], write a script that prints: the length, sum, average (to 2 decimal places), minimum, and maximum.

Show solution
nums = [23, 45, 12, 67, 34, 89, 2]

print(f"Length:  {len(nums)}")
print(f"Sum:     {sum(nums)}")
print(f"Average: {sum(nums) / len(nums):.2f}")
print(f"Min:     {min(nums)}")
print(f"Max:     {max(nums)}")
# Length: 7, Sum: 272, Average: 38.86, Min: 2, Max: 89
02 Remove Duplicates

Write code that takes [1, 2, 2, 3, 4, 4, 4, 5] and produces a new list with duplicates removed, preserving the original order. Don't use set() (that doesn't preserve order in older Python).

Show solution
original = [1, 2, 2, 3, 4, 4, 4, 5]
unique = []

for item in original:
    if item not in unique:
        unique.append(item)

print(unique)  # [1, 2, 3, 4, 5]

# Alternative (Python 3.7+ — dict preserves insertion order)
unique2 = list(dict.fromkeys(original))
print(unique2)  # [1, 2, 3, 4, 5]
03 Matrix Flatten

Given a 2D list (matrix) [[1, 2, 3], [4, 5, 6], [7, 8, 9]], flatten it into a single list [1, 2, 3, 4, 5, 6, 7, 8, 9]. Try both a nested loop approach and a list comprehension.

Show solution
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Approach 1: nested for loop
flat = []
for row in matrix:
    for item in row:
        flat.append(item)
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Approach 2: list comprehension
flat2 = [item for row in matrix for item in row]
print(flat2)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Approach 3: extend in a loop
flat3 = []
for row in matrix:
    flat3.extend(row)
print(flat3)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]