Data Structures: Beginner
Lists and tuples: position is the whole point
A list stores items in numbered slots starting at 0. The first value you append lands in slot 0, the next in slot 1, and nothing moves unless you remove something. A tuple works the same way, built once with its items already seated. Both answer steps[0] with the item that went in first, every time the program runs, on every computer.
Appending report put it in slot 3, the next free position, and the earlier 3 items stayed where they were. The tuple answered slot 2 with 48 because that is where 48 sat when the tuple was written.
Dicts: keys come back in the order they went in
Line 7 changed the value under load and the key stayed in front. Deleting it and adding it back moved it to the end, since the second add is a new insertion with no memory of the old one.
Sets: no slots, so no order
A set keeps no slots at all. It stores each item at a spot computed from the item's own value, which is what lets 30 in tags answer without scanning, and it means the order you added items plays no part in the order Python prints them. Small integers often come out in numeric order on the standard interpreter, so {3, 1, 2} prints as {1, 2, 3}, which looks like sorting but is a side effect of where those numbers land. Strings come out in an order that can change between one run and the next, so code that depends on set order can pass on one run and fail on the next.
1arrivals = [30, 10, 20]2in_order = []3no_order = set()45for value in arrivals:6 in_order.append(value)7 no_order.add(value)89print("list:", in_order)10print("set:", no_order)11print("first in list:", in_order[0])12print("30 in set:", 30 in no_order)
Same 3 values, added in the same sequence: the list keeps slot positions, the set does not.
Step through it and watch in_order grow at the end while no_order places each value where its number sends it. The 2 collections hold the same 3 integers and answer different questions: the list knows what came first, the set knows what is present.
Both membership checks answered at once, because answering them is what a set is built for. Add a line print(tags[0]) and the program stops there with TypeError: 'set' object is not subscriptable. There is no slot 0 to read, and the error names the operation the set refused: subscripting, the word Python uses for square brackets written after a value.
| Collection | Remembers insertion order | Reads by position |
|---|---|---|
| list | yes | yes |
| tuple | yes | yes |
| dict | yes, for keys | no, reads by key |
| set | no | no |
batch[2], reads slot 2 when batch is a list or a tuple. On a dict the same brackets look up the key 2, and the lookup stops with a KeyError unless 2 is one of its keys. A set refuses the brackets entirely.Practice
What does this program print?
The drill below depends on how each collection starts and grows. An empty list is [] and grows with append, which puts the item in the next free slot. An empty set is set(), since {} builds an empty dict, and it grows with add, which files the item by its value. A set has no append and a list has no add, so pairing a collection with the other one's method stops the program with an AttributeError.
| Question | list | set |
|---|---|---|
| Empty one is written | `[]` | `set()` |
| Adds an item with | `append` | `add` |
| A repeated value | takes a new slot | is ignored |
| Reading slot 0 | returns the first item | raises TypeError |
Editing a list, a dict and a set in place
Editing in place means the operation changes the existing object rather than building a new one. scores.append(90) returns nothing useful and leaves scores longer by 1 item. The same holds for metrics["errors"] = 5 on a dict and seen.add(42) on a set. In the animation below, one dict gains a key, has a value replaced and then loses a key, and the name metrics points at that same dict the whole time.
1metrics = {"requests": 1000, "errors": 5}2print("Initial:", metrics)34# Add new key5metrics["latency_ms"] = 45.26print("After adding:", metrics)78# Update existing key9metrics["requests"] = 105010print("After update:", metrics)1112# Remove a key13del metrics["errors"]14print("After delete:", metrics)
Because the object itself changes, every name bound to it sees the edit. A function that receives a list as a parameter receives the caller's list, so an append inside the function shows up outside it without any return.
pending has 3 items after the call even though record returned nothing. The function edited the one list that both names refer to.
A tuple cannot be edited
A tuple has neither append nor remove, and it does not allow assignment into a slot. Calling a method it lacks raises an AttributeError, because the tuple has no attribute by that name. Assigning into a slot raises a TypeError, because the tuple refuses item assignment. What you can do is point the name at a different tuple, which leaves the old tuple exactly as it was and is a separate action from editing it.
| Attempt on release | What Python reports |
|---|---|
| `release.append(2026)` | `AttributeError: 'tuple' object has no attribute 'append'` |
| `release.remove(2025)` | `AttributeError: 'tuple' object has no attribute 'remove'` |
| `release[0] = 2026` | `TypeError: 'tuple' object does not support item assignment` |
Why want something that cannot change
Python accepted the tuple as a key, and reading it back with the same tuple returned the stored value. If the same coordinates are written as the list locations[[40.7, -74.0]] = "New York", the program stops with TypeError: unhashable type: 'list'. Unhashable is the name Python gives to values that can change and therefore cannot be filed by their contents.
| Collection | Add | Remove | Replace one item |
|---|---|---|---|
| list | `append` | `remove`, `pop` | assign to a slot |
| dict | assign to a new key | `del`, `pop` | assign to the key |
| set | `add` | `remove`, `discard` | `remove` then `add` |
| tuple | none | none | none, build a new tuple |
- ▸Editing in place:
cart.append(item). The object grows, and every name bound to it sees the new item. - ▸Rebinding:
release = (2025, 1, 15). A new object is built and the name moves to it, while the old object is untouched. - ▸A tuple allows rebinding only. Every other collection here allows both.
Practice
What does this program print?
An AttributeError raised on a method call usually traces back to the line that built the collection. Parentheses there make a tuple, and square brackets around the same items make a list in the same order, so changing the brackets on that one line is enough for every later append to succeed. Keep the parentheses only when the items are meant to stay fixed.
- ("extract", "clean") is a tuple
- Keeps its items in order
- append raises AttributeError
- Accepted as a dict key
- ["extract", "clean"] is a list
- Keeps its items in order
- append adds to the end
- Refused as a dict key
Each collection type doubles as a function. list(x) takes any collection and builds a new one of the named type holding the same items, and tuple(x) and set(x) work the same way. The source is untouched. What differs is which guarantees the new collection makes, and that is where values keep or lose something on the way across.
What survives the trip
| From and to | Keeps | Loses |
|---|---|---|
| list to tuple | order, every duplicate | the ability to edit |
| tuple to list | order, every duplicate | the fixed guarantee |
| list to set | one copy of each distinct value | order, duplicates, slot numbers |
| set to list | the distinct values, now in slots | nothing further; the order is the set's |
| dict to list | the keys, in insertion order | the values |
| dict to set | the distinct keys | the values and the order |
All 5 readings went in. The tuple reports 5 with the order intact. The set reports 3 and prints {9, 2, 4}, which is the slot order inside the set and has no relation to the order the readings arrived. The last line shows readings still holds all 5.
1readings = [4, 2, 4, 9, 2]23as_set = set(readings)4back_to_list = list(as_set)5locked = tuple(readings)67print("original:", readings)8print("as set:", as_set)9print("back:", back_to_list)10print("locked:", locked)11print("same length:", len(readings) == len(back_to_list))
The round trip through a set returns a list, but with 3 items in the set's slot order, not the 5 you started with.
Watch back_to_list appear: it is a real list with a slot 0, and back_to_list[0] would work. What it cannot give back is the original arrival order or the 2 lost duplicates, because the set never had them.
A dict converts to its keys
Handing a dict to list() or set() gives you the keys only, in insertion order for the list. The values stay behind in the dict. This is the usual way to get a dict's keys into something you can index.
Building a dict from pairs
dict() runs the other direction. Give it a list of 2-item tuples and each tuple's first item becomes a key, with its second item as that key's value.
Conversion never edits the source, so to drop duplicates and keep working with a list, assign the result back: readings = list(set(readings)). After that line the order is the set's order, and the duplicate count is gone for good.
Practice
Both exercises hinge on what set() throws away and what list() can and can't restore.
What does this program print?
Converting a dict to a set never shrinks it, because its keys were unique to begin with, so len(set(inventory)) always equals len(inventory). Converting it to a list is what makes reading by position possible: square brackets on the dict look up a key, while square brackets on the list read a slot.
| Expression | Result |
|---|---|
| `inventory["nut"]` | 300 |
| `inventory[0]` | stops with `KeyError: 0` |
| `list(inventory)[0]` | `'bolt'` |
| `len(set(inventory))` | 3 |
Lists and tuples keep every copy
The list took 4 items and len reported 4. remove deleted the first home only, so 2 copies remain. Each copy is a real item in its own slot. min counts them all, as do max and sum: sum([5, 5, 5]) is 15, and a tuple behaves the same way.
A set collapses copies
Of the 4 values added, 2 survive. add on a value already present does nothing and raises nothing, which is what makes a set the right place to record whether something has been seen rather than how often.
Dict keys: the last one wins
Write a dict literal with the same key twice and Python builds it left to right, so the second value replaces the first. The key keeps the position where it first appeared. The same rule governs dict() given a list of pairs, and plain assignment to a key that already exists.
Python raises nothing, and len reports 2. A config literal with a repeated key silently discards the earlier value, and the mistake only surfaces when the program behaves as if retries were 5. The pairs example keeps a in front with the value 10 from the later pair.
Using the rule on purpose: counting
Because keys are unique, a dict is the natural place to count how often each item of a list appears: the item becomes the key and its count the value. in checks whether the key exists before deciding whether to start at 1 or add 1 to the count already there.
1clicks = ["home", "cart", "home", "home", "cart"]2counts = {}34for page in clicks:5 if page in counts:6 counts[page] = counts[page] + 17 else:8 counts[page] = 1910print(counts)11print("distinct pages:", len(counts))12print("total clicks:", len(clicks))
Each repeated page lands on the key it already has, so the dict never grows past the number of distinct pages.
The list reports 5 because it kept every click. The dict reports 2 because home and cart are the only keys that ever existed, no matter how many times each was seen.
| Collection | Written as | Stored as | Length |
|---|---|---|---|
| list | `[2, 2, 3]` | `[2, 2, 3]` | 3 |
| tuple | `(2, 2, 3)` | `(2, 2, 3)` | 3 |
| set | `{2, 2, 3}` | `{2, 3}` | 2 |
| dict | `{2: "a", 2: "b"}` | `{2: 'b'}` | 1 |
Practice
What does this program print?
Trace the counting loop on 3 clicks and the 2 branches separate cleanly. A page's first visit finds no key, so the count starts at 1, and the key takes its position in the dict at that moment. Every later visit finds the key and adds 1 to the value under it, leaving the position alone. Flip the test to not in and the branches swap jobs with it: the first branch starts the count and the else adds 1.
| Page | page in counts | Branch taken | counts afterwards |
|---|---|---|---|
| `home` | False | start at 1 | `{'home': 1}` |
| `cart` | False | start at 1 | `{'home': 1, 'cart': 1}` |
| `home` | True | add 1 | `{'home': 2, 'cart': 1}` |
| Need | Pick | Because |
|---|---|---|
| Steps of a pipeline that run in sequence and may grow | list | order matters and steps get added |
| Latitude and longitude of one warehouse | tuple | 2 fixed values, usable as a dict key |
| Stock level per product name | dict | lookup by label |
| User ids already emailed today | set | membership test, no repeats |
| The 30 tracks of a playlist in play order | list | order matters and tracks move |
| Column names a header row must match | tuple | a fixed fact nothing should edit |
- Arrival order carries meaning
- Repeated values are real data
- You read items by slot number
- Items are appended at the end
- The only question is whether a value is present
- Repeated values should vanish
- Order is irrelevant
- The items are numbers or strings (tuples work too)
Putting 3 types together
Real tasks mix types, and the mix follows the same questions. The animation walks a list of click events, uses a set to remember which users have already appeared, and builds a list of each user's first event. The list preserves arrival order, the set answers not in without scanning, and the dict inside each event holds that event's fields by name.
1events = [2 {"user": 1, "action": "click"},3 {"user": 2, "action": "view"},4 {"user": 1, "action": "buy"},5 {"user": 3, "action": "click"},6 {"user": 2, "action": "click"},7]89seen_users = set()10first_events = []1112for event in events:13 user = event["user"]14 if user not in seen_users:15 seen_users.add(user)16 first_events.append(event)1718print("events:", len(events))19print("users:", len(seen_users))20print("first:", first_events)
From 5 events by 3 distinct users, the list ends with exactly one event per user, in the order they first appeared.
- Run through the 4 questions before writing the first bracket.
- Use a tuple for a record whose shape never changes.
- Keep a set beside a list when you need both arrival order and a fast membership test.
- Count repeats with a dict: the item is the key and the count is the value.
- Rely on the order a set prints in.
- Index a dict with a position number.
- Scan a list with in when a set would answer the same question without a scan.
- Repeat a key in a dict literal and expect both values to survive.
Capstones
What does this program print?
A program stores the 7 day names and later reads the name for day number 3 with names[3]. The program never edits the day names after they are stored. Which collection fits best?
The third capstone builds 2 collections from the same list of readings, and each answers a different count. len of a set counts distinct values, while len of a tuple counts every reading, repeats included. max returns the same highest value from either one, because dropping repeats never removes the largest value, only extra copies of it.
The last capstone depends on what square brackets mean on a dict built from pairs. dict() makes the first item of each pair a key, so a dict built from (name, quantity) pairs is read with a name. A position number between the brackets asks for a key that is that number, and when no such key exists the lookup stops with a KeyError naming the missing key.
> Netflix builds each member's Continue Watching row: the titles must appear in the order they were most recently played, each title's runtime and release year never change, the row must never show the same title twice, and the resume position has to be found by title id in one lookup.
not in without scanning the list.list() on the dict of positions hands back the ids in the order they were first inserted.Four collections, four promises: which keep order, which can change, which drop duplicates, and when to use each.
- Category
- Python
- Difficulty
- beginner
- Duration
- 42 minutes
- Challenges
- 19 hands-on challenges
Topics covered: Which Collections Keep Their Order, Which Collections Can Change, Converting Between Collections, Which Collections Allow Duplicates, Choosing the Right Type
Lesson Sections
- Which Collections Keep Their Order (concepts: Lists)
Every collection you have met so far takes items one at a time. This section asks whether the collection remembers the sequence those items arrived in, because that decides whether a position like slot 0 means anything when you read the items back out. Lists and tuples: position is the whole point Dicts: keys come back in the order they went in A dict stores pairs, and since Python 3.7 the language guarantees that the keys come back in the order they were first inserted. Reassigning a value to a
- Which Collections Can Change (concepts: Tuples)
Of the 4 collections, 3 can be edited after they exist: you can add items to them or take items out, and the collection keeps its identity through every change. The tuple is the exception. Once built, it holds exactly the items it was built with, for as long as it exists, and every attempt to alter it raises an error. Editing a list, a dict and a set in place A tuple cannot be edited Line 3 replaced the whole tuple, which is allowed because assignment binds a name and never touches the object. T
- Converting Between Collections (concepts: Dictionaries)
What survives the trip Read each row as a promise. A conversion to a tuple or a list keeps every item in its slot, so the length never changes. A set keeps one copy of each distinct value and throws away the slots, so the length can only shrink or stay the same. When a dict is the source, only its keys make the trip and the values stay behind. A dict converts to its keys Building a dict from pairs Practice
- Which Collections Allow Duplicates (concepts: Sets)
A duplicate is the same value appearing more than once. Lists and tuples store every copy and count every copy. A set stores one. A dict sits in between: keys are unique, values are free to repeat, and giving a dict the same key twice raises no error and keeps the last value. Lists and tuples keep every copy A set collapses copies Dict keys: the last one wins Using the rule on purpose: counting Practice The first exercise mixes a set literal and a dict literal with repeats. The second has you wr
- Choosing the Right Type (concepts: Data Types)
Choosing a collection comes down to 4 questions. Order and change were covered in the earlier sections, as were duplicates, and the question about lookup by label is what a dict's keys are for. Ask them in the order below and the answer usually falls out by the second or third. The questions narrow the field one at a time. Take a version number such as 3.9.7, stored as 3 integers. Position matters, since 3 means the major version only in slot 0, so question 1 leaves a list or a tuple. The parts