Collections
Introduction
Every type in 1.1 Objects and Types held a single value. The real power of a programming language, though, comes from gathering many values together and treating the whole group as one thing — a roster of students, the pixels of an image, a table of measurements. The types that do this are the containers, and they are where most real Python programs spend their time.
This page walks the three container families, distinguished by the question how are the items kept? A sequence keeps its items in order, so each has a position you can index by. A set keeps its items unique, throwing away duplicates and forgetting order. A mapping pairs each item with a key, so you look things up by name rather than by position. Once all three are in hand, Section 4 returns to mutability — the second axis from 1.1 — because it quietly governs how they all behave, and Section 5 covers slicing, reading whole subsequences at once. The code here is live, so run and tweak it.
1. Sequences: lists, tuples, and ranges
The containers you will use most are ordered sequences. The two workhorses are the list and the tuple. Both hold an ordered collection of items; the headline difference is mutability. A list is written with square brackets [] and is mutable; a tuple is written with parentheses () and is immutable.
The example below builds one of each and prints them — notice both can mix items of different types.
Example: a list and a tuple
A third sequence, range, represents an arithmetic progression of integers without storing them all — range(1, 101) stands for 1 through 100 but holds only its start, stop, and step. It is the idiomatic way to generate number sequences, often handed straight to list() or tuple().
Example: generating numbers with range
range takes up to three arguments — range(stop), range(start, stop), and range(start, stop, step) — and, exactly like a slice, the stop value is excluded. A negative step counts downward, and an empty range is perfectly legal.
Example: the forms of range
Exercise: building sequences
- Create a list of a few items of different types; print the whole list and one element by index (indexing starts at 0).
- Do the same with a tuple.
- Use
range()to build a list, and then a tuple, of the numbers 1 to 100.
Sequences share a common toolkit — indexing, membership tests with in, length with len(), and slicing (Section 5). They also have methods, which are functions attached to an object and called with a dot.
Key concept: method
A method is a function that belongs to an object and is called on it with
dot notation, as in my_list.append(4). Because lists are mutable, many list
methods change the list in place; tuples, being immutable, have no such
methods.
The most common list methods are append() (add one item to the end), extend() (add all items from another iterable), and pop() (remove and return the item at an index, last by default). The + operator concatenates two sequences into a new one.
2. Sets
Where a sequence carefully remembers the order of its items, a set deliberately forgets it. A set is an unordered collection of distinct items, mirroring the mathematical set: {1, 2} is the same set as {2, 1, 1}. Sets are mutable; their immutable counterpart is frozenset. They shine at two jobs: fast membership tests and removing duplicates.
The example below shows both jobs at once, and how to build a set from another collection.
Example: creating sets and dropping duplicates
Because a set mirrors a mathematical set, it supports the familiar set operations, each available both as a method and as an operator.
Example: set operations
A set's elements must be hashable, which is why a set can hold numbers, strings, and tuples but not lists. When you need an immutable set — say, to use as a dictionary key or as a member of another set — reach for frozenset.
Deep dive: what does 'hashable' mean?
An object is hashable if it has a hash value that never changes during its
lifetime, which is what lets Python place it in the internal table that makes
set membership and dict lookup fast. Immutable built-ins (numbers, strings,
tuples of hashables) are hashable; mutable ones (lists, dicts, sets) are not —
which is exactly why a list cannot be a set element or a dict key, but a
frozenset can. This is the practical payoff of the mutable/immutable line
from Section 4.
Exercise: sets
- From
numbers = [1, 2, 2, 3, 4, 4, 5], produce a list of just the unique values. - With
a = {1, 2, 3, 4}andb = {3, 4, 5, 6}, compute the values inaorbbut not both. - Try adding a list to a set and read the error; then add a tuple instead and watch it work.
3. Mappings: the dictionary
The last of the three families abandons positions altogether and instead pairs each value with a key of your choosing. A dictionary (dict) stores key–value pairs and is the most important container in everyday Python. It is mutable, and it lets you look a value up by its key rather than by position.
The example below creates a dictionary two ways, looks a value up, and both adds and updates a pair.
Example: creating and using a dict
Looking up a missing key with [] raises a KeyError. When a key might be absent, get() returns None (or a default you supply) instead of crashing — and update() merges another dictionary in.
Example: safe lookup and merging
To walk a dictionary you iterate its keys(), values(), or items() — the last hands you the key and value together, which you will use constantly once you reach the for loop in 1.3.
Pitfall: dict keys must be hashable
A key must be hashable (see the set deep dive), so you may key by a number, string, or tuple — but never by a list. The value, by contrast, can be any object at all.
Exercise: dictionaries
- Build a dict mapping three names to ages, then print one person's age.
- Use
get()to look up a name that is absent, returning"unknown"instead of crashing. - Add a new person, then update an existing person's age.
4. Mutability and identity
We met mutability as one of the two axes in 1.1; now that we have the container types in hand, we can give it the attention it deserves, because it quietly governs how assignment, comparison, and even dictionary keys behave.
The defining difference between a list and a tuple shows up across several operations. The example below shows that you can read an element from either by index, but only a list lets you reassign or delete one. (The line that would fail on a tuple is left commented — uncomment it to see the error.)
Example: only the list can be edited
Mutability also explains a subtle behaviour of +=. The example below shows that for a mutable list, += changes the object in place so its identity is unchanged; for an immutable tuple, += must build a brand-new object, so its identity changes.
Example: += and object identity
Pitfall: two names, one mutable object
Because a name is only a label, binding b = a makes both names point to the
same object. If that object is mutable, a change through one name is visible
through the other:
This aliasing is harmless for immutable objects (you can never change them) but a classic source of bugs for mutable ones.
Visually, the two names share one list, so a change made through b is seen through a:
There is also a practical payoff: immutability is what makes an object usable as a dictionary key or a set member. Try to use a list as a key and Python refuses, because a key must not change underneath the dictionary.
Deep dive: is the mutable/immutable line really fundamental?
Partly. At the language level, mutability is a genuine, observable contract that every Python implementation honours, and it is why immutable objects can be hashed (and thus used as dict keys or set members). But the boundary has honest grey areas. A tuple is immutable, yet a tuple containing a list lets you mutate the inner list — the tuple's own references don't change, but what they point to can. And the reason a type is immutable is often a design and implementation choice (safety, sharing, optimisation, hashing) rather than a deep law. So: treat mutability as a real and useful concept, but understand that the line is drawn by Python's designers, with C-level mechanics underneath — not handed down by mathematics.
Exercise: mutability in action
- Set the second element of a list to
-1and confirm it works; try the same on a tuple and read the error. - Use
id()to check a list before and after an in-place change — does its identity hold? Then explain, in one sentence, why a tuple "edit" needs a new object.
5. Slicing: reading subsequences
Indexing reads one element; slicing reads a whole subsequence. The notation is sequence[start:stop:step], where start is the first index included (default 0), stop is the first index excluded, and step is the stride (default 1; a negative step walks backwards). The colons are required; the three numbers are each optional.
The example below slices a list a few different ways — change the numbers and rerun to build intuition.
Example: slicing a list
Exercise: slice syntax
Decide whether each is a valid slice used as seq[…], and say what it does:
1:2:1, 9:1:-1, 1.5:2.3:3.14, :-5:-1, ::-1.
Deep dive: a slice is itself an object
The notation start:stop:step builds a slice object, and you can make one
explicitly and reuse it:
So slice is a type, and xs[1:10:2] is sugar for xs[slice(1, 10, 2)].
Summary
The containers are where Python programs keep their data. Place any built-in collection on the two axes from 1.1 — what it holds and whether it can change:
| Category | Types | Mutable? |
|---|---|---|
| Sequences | list / tuple, range |
list mutable, tuple/range immutable |
| Sets | set / frozenset |
set mutable, frozenset immutable |
| Mappings | dict |
mutable |
Mutability is more than a label: it explains aliasing, the behaviour of +=, and why only immutable (hashable) objects can be dictionary keys or set members. With the types (1.1) and the containers (1.2) in hand, 1.3 Control Flow puts them to work — looping over them and testing them.