List Basics
Objectives
By the end of this chapter, you should be able to:
- Define what a list is in Python
- Access and reassign values in a list
- Use common built-in list methods
- Copy or reverse a list using slices
- Explain the difference between a shallow copy and a deep copy
💡 Why this matters: Almost every real program deals with a collection of things. Lists are Python’s default way to represent that, and you’ll use them constantly.
What Is a List?
A list is an ordered collection of elements. It can hold as many elements as you want, and they don’t all need to share a type:
number_list = [1, 2, 3, 4, 5]
string_list = ["a", "b", "c", "d"]
kitchen_sink_list = [4, "yo", None, False, True, ["another", "list"]]
Accessing and Reassigning Elements
Lists use bracket notation and a zero-based index: the first element is index 0:
my_list = ["a", 1, True]
my_list[0] # "a"
my_list[2] # True
my_list[3] # IndexError: there's no index 3 here
Reassigning an element works the same way, with =:
my_list = ["a", 1, True]
my_list[2] = False
my_list # ["a", 1, False]
Built-In List Methods
| Method | Does |
|---|---|
.append(x) |
Adds x to the end of the list |
.clear() |
Removes every element |
.copy() |
Returns a new, independent list with the same elements |
.count(x) |
Counts how many times x appears |
.extend(other_list) |
Appends another list’s elements, flattened in (not nested) |
.index(x) |
Returns the index of the first match; raises ValueError if not found |
.insert(i, x) |
Inserts x at index i |
.pop(i) |
Removes and returns the element at index i (last element if i is omitted) |
.remove(x) |
Removes the first occurrence of x; raises ValueError if not found |
.reverse() |
Reverses the list in place |
.sort() |
Sorts the list in place |
Here’s one example touching most of the table above, run in order on the same list:
fruits = ["banana", "apple", "cherry"]
fruits.append("date") # add "date" to the end
fruits.insert(1, "kiwi") # insert "kiwi" at index 1
fruits.index("cherry") # 3 - the position of "cherry"
fruits.count("apple") # 1 - "apple" appears once
fruits.sort() # sort in place, alphabetically
fruits
# ['apple', 'banana', 'cherry', 'date', 'kiwi']
fruits.reverse()
fruits
# ['kiwi', 'date', 'cherry', 'banana', 'apple']
fruits.pop() # removes and returns "apple", the last element
fruits.remove("kiwi") # removes the first "kiwi" it finds
fruits
# ['date', 'cherry', 'banana']
fruits.clear()
fruits # []
Two methods worth a closer look:
.copy() gives you a genuinely independent list: changes to the copy don’t affect the original:
original = [2, 3, 4]
duplicate = original.copy()
duplicate.remove(3)
duplicate # [2, 4]
original # [2, 3, 4] (untouched)
.extend() flattens the list you pass in, where .append() would nest it:
l = [1, 2, 3]
l.append([4])
l # [1, 2, 3, [4]]
l.extend([5, 6, 7])
l # [1, 2, 3, [4], 5, 6, 7]
.pop() and .remove() both raise an error under the wrong conditions: .pop() on an empty list raises IndexError, and .remove() raises ValueError if the value isn’t there at all.
empty = []
empty.pop() # IndexError: pop from empty list
empty.remove("x") # ValueError: list.remove(x): x not in list
Copying and Reversing with Slices
A slice grabs a portion of a list (or string), using list[start:end] or list[start:end:step]:
first_list = [1, 2, 3, 4, 5, 6]
first_list[0:1] # [1]
first_list[1:] # [2, 3, 4, 5, 6] (no end means "to the end")
first_list[:3] # [1, 2, 3] (no start means "from the beginning")
first_list[-1] # 6 (negative indexes count from the end)
first_list[-2:] # [5, 6]
first_list[::-1] # [6, 5, 4, 3, 2, 1] (a reversed copy)
first_list[::-2] # [6, 4, 2] (reversed, stepping by two)
list[:] is a common shorthand for “copy the whole list.”
Shallow Copies vs. Deep Copies
new_list = original does not copy anything: it just gives the same list a second name. Both names point at the same object in memory, so a change through either one shows up through the other:
original = [1, 2, 3]
alias = original
alias.append(4)
original # [1, 2, 3, 4] (changed too, since alias IS original)
.copy() (and list[:]) fixes that for a flat list, but only one level deep. If a list contains other mutable objects, like nested lists, both the original and the copy still share those inner objects:
import copy
original = [1, 2, [3, 4]]
shallow = original.copy()
shallow[0] = 99 # doesn't affect original (top-level values are independent)
shallow[2].append(5) # DOES affect original (the nested list is shared)
original # [1, 2, [3, 4, 5]]
For a fully independent copy, including every nested list, dict, or other mutable object inside it, use copy.deepcopy() instead:
import copy
original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)
deep[2].append(5)
original # [1, 2, [3, 4]] (completely untouched this time)
Reach for .copy()/list[:] for a flat list of simple values, and copy.deepcopy() whenever your list contains other lists, dicts, or objects you don’t want shared.
Try It
- Build a list of five items and practice
.append(),.remove(),.pop(), and.sort()on it. - Use a slice to create a reversed copy of a list without mutating the original.
- Predict what
original.copy()gives you versusnew_list = original, then check by modifying one and seeing what happens to the other. - Build a list containing a nested list, make a shallow copy with
.copy(), modify the nested list through the copy, and confirm the original changed too. Then repeat withcopy.deepcopy()and confirm it didn’t.
Recap
- Lists are ordered, mutable, zero-indexed collections that can hold any mix of types.
.append()/.insert()add elements;.pop()/.remove()/.clear()take them away;.sort()/.reverse()reorder in place;.copy()/.index()/.count()read without mutating.- Slices (
list[start:end:step]) grab a portion, copy (list[:]), or reverse (list[::-1]) a list. new_list = originaldoesn’t copy anything: both names share the same list..copy()/list[:]make a shallow copy (nested objects are still shared);copy.deepcopy()makes every level independent.
Next lesson: looping over lists efficiently, and list comprehension, one of Python’s most useful shortcuts.