If you only ever learned one collection in Python, you'd reach for list 80% of the time and survive. It's an ordered, mutable, dynamically-sized sequence that holds anything — including other lists. Lists are written with square brackets and can be empty, homogeneous, or wildly mixed.
Creating lists
Three common ways: literal syntax, the list() constructor, and comprehensions (which we cover in lesson 6). Literal is the one you'll write most often.
Indexing — and Python's negative trick
Indexing starts at 0, like most languages. The trick Python adds: negative indices count from the end. my_list[-1] is the last element. my_list[-2] is the second-to-last. There's no special syntax — negative numbers just work.
Slicing — the syntax that quietly does the work of three for-loops
Slicing is my_list[start:stop:step]. Each part is optional. my_list[:5] is the first five. my_list[-3:] is the last three. my_list[::-1] is the whole list reversed. Slicing returns a new list — the original is untouched. This is one of the highest-leverage syntactic features in the language; learn it once and you'll skip writing dozens of loops over your career.
Lists are mutable — and that has a price
You can change a list after creation: append, extend, slice-assignment, del. That's the joy. The price: a list can't be a dict key, can't be inside a set, and if you pass a list to a function, the function can mutate your list.
Warning: The classic gotcha — def f(x=[]). The default list is shared across all calls because it's evaluated once, at function definition time. Use def f(x=None): if x is None: x = [] instead.
Copying — the shallow vs deep distinction
list2 = list1 doesn't copy — both names point at the same list. list2 = list1[:] or list2 = list1.copy() makes a shallow copy: a new outer list, but the inner objects are still shared. For nested structures, reach for copy.deepcopy().
Pythonic Way: If you mean "a sequence of things," reach for list. Don't reach for tuple just because the values won't change — there's a sharper rule for that, and we cover it in lesson 3.
Code
Three ways to create a list·python
# 1. Literal — the way you'll write 90% of the time
fruits = ["apple", "banana", "cherry"]
# 2. Constructor — turns any iterable into a list
letters = list("hello") # ['h', 'e', 'l', 'l', 'o']
range_list = list(range(5)) # [0, 1, 2, 3, 4]
# 3. Empty, then build up
result = []
result.append(1)
result.append(2)
print(result) # [1, 2]
Negative indexing — counting from the end·python
scores = [88, 92, 75, 100, 64]
print(scores[0]) # 88 first element
print(scores[-1]) # 64 last element
print(scores[-2]) # 100 second-to-last
# IndexError if you go past the end in either direction
# print(scores[5]) # IndexError
# print(scores[-6]) # IndexError
Slicing — start : stop : step·python
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[2:5]) # [2, 3, 4] stop is EXCLUSIVE
print(nums[:3]) # [0, 1, 2] from the start
print(nums[7:]) # [7, 8, 9] to the end
print(nums[::2]) # [0, 2, 4, 6, 8] every second
print(nums[::-1]) # [9, 8, ..., 0] reversed
print(nums[-3:]) # [7, 8, 9] last three
# Slicing always returns a NEW list. nums is untouched.
print(nums) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Slice ASSIGNMENT — replacing a range in place·python
data = [10, 20, 30, 40, 50]
# Replace 3 elements with 5 new ones
data[1:4] = ["a", "b", "c", "d", "e"]
print(data) # [10, 'a', 'b', 'c', 'd', 'e', 50]
# Delete a slice
data[1:4] = []
print(data) # [10, 'd', 'e', 50]
# Insert without removing — slice of length 0
data[2:2] = ["X", "Y"]
print(data) # [10, 'd', 'X', 'Y', 'e', 50]
Aliases vs. shallow copies vs. deep copies·python
import copy
original = [[1, 2], [3, 4]]
alias = original # SAME list — both names share
shallow = original[:] # new outer, same inner refs
deep = copy.deepcopy(original) # new outer AND new inner
original[0].append(99)
print(alias) # [[1, 2, 99], [3, 4]] — shares with original
print(shallow) # [[1, 2, 99], [3, 4]] — inner list is shared!
print(deep) # [[1, 2], [3, 4]] — fully independent
The mutable default argument trap·python
# DON'T DO THIS
def bad(item, history=[]):
history.append(item)
return history
print(bad("a")) # ['a']
print(bad("b")) # ['a', 'b'] <- history was REUSED
print(bad("c")) # ['a', 'b', 'c']
# DO THIS
def good(item, history=None):
if history is None:
history = []
history.append(item)
return history
Create a list of 10 integers from 1 to 10. Then, using only slicing (no loops, no for, no comprehensions): (a) print the first 3, (b) print the last 3, (c) print every other element starting from the second, (d) print the list reversed. Confirm none of these mutate the original — print the original at the end and verify it's unchanged.
Progress
Progress is local-only — sign in to sync across devices.