Over the next few chapters you'll meet Python's three basic sequence types: lists, tuples, and ranges. They all store ordered collections of items — the differences are in mutability, syntax, and use case.
✨Big idea: a list is an ordered, mutable, zero-indexed sequence that can hold any mix of types — strings, numbers, booleans, even other lists. It's the workhorse data structure of Python.
| Trait | Meaning |
|---|---|
| Ordered | Items keep their insertion order. [1, 2, 3] is not the same as [3, 2, 1]. |
| Mutable | You can change, add, and remove items in place — without rebuilding the list. |
| Zero-indexed | The first element is at index 0, the second at 1, and so on. |
Use square brackets with comma-separated values:
cities = ['Los Angeles', 'London', 'Tokyo']
numbers = [1, 2, 3, 4, 5]
mixed = ['Alice', 25, True, 3.14, None]
empty = []Lists can mix types freely — Python doesn't restrict you to one type per list.
Use the index number inside square brackets:
cities = ['Los Angeles', 'London', 'Tokyo']
cities[0] # 'Los Angeles' ← first element (index 0!)
cities[1] # 'London'
cities[2] # 'Tokyo'Negative indices count from the end:
cities[-1] # 'Tokyo' ← last element
cities[-2] # 'London' ← second to last
cities[-3] # 'Los Angeles' ← third to last (first)flowchart LR
IDX0["0"] --- LA["'Los Angeles'"]
IDX1["1"] --- LON["'London'"]
IDX2["2"] --- TOK["'Tokyo'"]
LA --- NEG3["-3"]
LON --- NEG2["-2"]
TOK --- NEG1["-1"]
Memory hook: list[0] is the first item, not the second. list[-1] is always the last item, no matter the list size.
Another way to create a list is to convert an existing iterable using list():
developer = 'Jessica'
list(developer) # ['J', 'e', 's', 's', 'i', 'c', 'a']
list(range(5)) # [0, 1, 2, 3, 4]
list((1, 2, 3)) # [1, 2, 3] (tuple → list)
list() # [] (empty list)An iterable is anything you can loop over one item at a time — strings, tuples, ranges, dictionaries, sets, files, etc. You'll meet loops formally soon.
Get the number of items with the built-in len():
numbers = [1, 2, 3, 4, 5]
len(numbers) # 5
len([]) # 0
len([[1, 2], [3, 4]]) # 2 (two nested lists)Because lists are mutable, you can replace any element by its index:
programming_languages = ['Python', 'Java', 'C++', 'Rust']
programming_languages[0] = 'JavaScript'
print(programming_languages)
# ['JavaScript', 'Java', 'C++', 'Rust']This edits the list in place — no new list is created. Compare with strings, which are immutable: s[0] = 'X' raises TypeError.
If you use an index that doesn't exist, Python raises IndexError:
programming_languages = ['Python', 'Java', 'C++', 'Rust']
programming_languages[10] = 'JavaScript'
# IndexError: list assignment index out of rangeValid positive indices: 0 to len(list) - 1. Valid negative indices: -1 to -len(list).
developer = ['Jane Doe', 23, 'Python Developer']
del developer[1]
print(developer) # ['Jane Doe', 'Python Developer']del removes the element at the given index and shifts everything after it one position left. You'll meet other removal methods (remove(), pop()) in the next chapter.
Check whether a value exists in a list with in — returns a boolean:
programming_languages = ['Python', 'Java', 'C++', 'Rust']
'Rust' in programming_languages # True
'JavaScript' in programming_languages # False
'JavaScript' not in programming_languages # Truein compares using ==, so it works for strings, numbers, and any object with proper equality.
Lists can contain other lists — great for tables, matrices, grouped data:
developer = ['Alice', 25, ['Python', 'Rust', 'C++']]Access the inner list with one index, then drill in with another:
developer[2] # ['Python', 'Rust', 'C++']
developer[2][0] # 'Python' ← first language
developer[2][1] # 'Rust' ← second language
developer[2][-1] # 'C++' ← last languageThink of each […] as stepping one level deeper into the nested structure.
flowchart TD
DEV["developer"] --> A["[0] 'Alice'"]
DEV --> AGE["[1] 25"]
DEV --> LANGS["[2] [...]"]
LANGS --> P["[0] 'Python'"]
LANGS --> R["[1] 'Rust'"]
LANGS --> C["[2] 'C++'"]
Unpacking spreads list values into separate variables in one statement:
developer = ['Alice', 34, 'Rust Developer']
name, age, job = developer
print(name) # 'Alice'
print(age) # 34
print(job) # 'Rust Developer'The number of variables must match the number of items — or use * to capture the rest.
developer = ['Alice', 34, 'Rust Developer']
name, *rest = developer
print(name) # 'Alice'
print(rest) # [34, 'Rust Developer']*rest collects everything else as a list. It can go at the start, middle, or end:
first, *middle, last = [1, 2, 3, 4, 5]
# first = 1, middle = [2, 3, 4], last = 5
*head, tail = [10, 20, 30]
# head = [10, 20], tail = 30developer = ['Alice', 34, 'Rust Developer']
name, age, job, city = developer
# ValueError: not enough values to unpack (expected 4, got 3)With *, this restriction relaxes — you only need at least as many fixed names as fixed values.
Access a portion of a list with the slice operator : (just like strings):
desserts = ['Cake', 'Cookies', 'Ice Cream', 'Pie', 'Brownies']
desserts[1:4] # ['Cookies', 'Ice Cream', 'Pie']start— inclusive (default0)stop— exclusive (default end)step— stride (default1)
desserts[:3] # ['Cake', 'Cookies', 'Ice Cream'] ← first three
desserts[2:] # ['Ice Cream', 'Pie', 'Brownies'] ← from index 2 to end
desserts[::2] # ['Cake', 'Ice Cream', 'Brownies'] ← every other item
desserts[::-1] # ['Brownies', 'Pie', 'Ice Cream', 'Cookies', 'Cake'] ← reversed!numbers = [1, 2, 3, 4, 5, 6]
numbers[1::2] # [2, 4, 6]- Start at index
1(the first even number2). - Omit
stop→ go all the way to the end. step = 2→ grab every second item.
Slices never raise IndexError. Out-of-range stops are silently clipped: [1, 2, 3][:100] returns [1, 2, 3]. Use indexing for single items and slicing for ranges.
flowchart TD
WANT{"What do you want?"} --> ONE{"A single item?"}
ONE -- Yes --> IDX["✅ Use indexing<br>list[i]<br>Returns: the element"]
ONE -- No --> MANY["✅ Use slicing<br>list[a:b:c]<br>Returns: a new list"]
IDX --> BOUNDS["⚠️ IndexError if out of range"]
MANY --> SAFE["✅ No error — silently clipped"]
developer = ['Alice', 34, ['Python', 'Rust', 'C++']]
# Read
print(developer[0]) # 'Alice'
print(developer[-1]) # ['Python', 'Rust', 'C++']
print(developer[-1][1]) # 'Rust'
# Update (mutable!)
developer[1] = 35
print(developer) # ['Alice', 35, ['Python', 'Rust', 'C++']]
# Check membership
print('Alice' in developer) # True
print('Python' in developer[-1]) # True
# Remove
del developer[1]
print(developer) # ['Alice', ['Python', 'Rust', 'C++']]
# Unpack
name, langs = developer
print(name) # 'Alice'
print(langs) # ['Python', 'Rust', 'C++']
# Slice
print(langs[:2]) # ['Python', 'Rust']
print(langs[::-1]) # ['C++', 'Rust', 'Python']- ✅ Use a list when order matters and you may need to modify the collection.
- ✅ Use
len()before risky indexing if the list might be empty. - ✅ Prefer
infor membership checks — cleaner than manual loops. - ✅ Use negative indices (
-1) for "last item" instead oflist[len(list) - 1]. - ✅ Use unpacking to make multi-value assignments expressive:
x, y, z = pointreads better thanx = point[0]; y = point[1]; z = point[2]. - ✅ Use slicing to take portions — it's safer than indexing because it never raises out-of-range errors.
- ✅ For very deeply nested data, consider giving the inner lists names (or using dictionaries) for readability.
- ❌ Counting from 1 instead of 0 —
list[1]is the second element, not the first. - ❌ Trying to assign to an out-of-range index →
IndexError. Useappend()to grow a list. - ❌ Confusing
del list[i]withlist.remove(value)—delis by index,remove()is by value. - ❌ Unpacking with the wrong number of variables →
ValueError. Use*restfor variable-length unpacking. - ❌ Slicing with a stop lower than start but no negative step → you get an empty list silently.
- ❌ Modifying a list while iterating over it — subtle bugs; covered in the loops chapter.
- ❌ Treating lists as if they were immutable like strings —
list[i] = xworks;str[i] = xerrors.
- Which of the following is true about lists? → They are zero-based indexed.
- Which is the correct way to access the second element in
cities = ['Los Angeles', 'London', 'Tokyo']? →cities[1] - Which is the correct way to access the second element from the end of
numbers = [1, 2, 3, 4, 5, 6]? →numbers[-2]
- A list is an ordered, mutable, zero-indexed sequence — the workhorse data structure of Python.
- Create lists with
[...]literals or thelist()constructor (converts any iterable). list[0]is the first item;list[-1]is the last.len(list)→ number of items.x in list→ boolean membership check.- Lists are mutable:
list[i] = valueupdates in place. Out-of-range →IndexError. del list[i]removes an item by index.- Lists can be nested — chain indices to drill deeper:
data[2][1]. - Unpacking assigns many variables at once:
a, b, c = list. Use*restto catch the remainder. - Slicing
list[start:stop:step]returns a new list — never raises out-of-range errors. - Next up: list methods — the built-in tools that make lists really sing.
➡️
Next chapter → Python List Methods — append, pop, sort & More
Lists are powerful because of their methods. Meet append, extend, insert, remove, pop, clear, sort, reverse, and index — plus the crucial sort() vs sorted() distinction.