Comparing lists, tuples, sets, and dictionaries by order, uniqueness, lookup, and mutability — and learning to design a program's data before writing its code
You now know Python's four main built-in collection types: lists, tuples, sets, and dictionaries. You have also built nested structures and used them to model real information. What you have not yet practiced is the most important skill that comes after knowing the tools: choosing the right tool for the job.
This chapter teaches you how to think before you code. You will compare structures by their properties, practice designing data representations on paper before writing a line of Python, and learn to recognize when a first design needs to be reworked. By the end, you should be able to look at a problem, sketch a data shape, justify your choice, and write programs that are clearer because the data is organized well.
A teacher wants to track which students have submitted an assignment, a store wants to look up a product's price by name, and a researcher wants to count how many times each word appears in a document. Each of these problems could technically be solved with a list — but each one has a different structure that fits it far better. What separates a program that merely works from a program whose data is organized well? By the end of this chapter, you will have a framework for answering that question every time you sit down to write code.
By the end of this chapter, you should be able to:
When you are new to programming, any structure that works feels like a good choice. If you can store ten names in a list, you use a list. If you have seen dictionaries solve a problem like this before, you try a dictionary. The program runs. You move on.
This approach has a real cost. Programs that use the wrong structure for their data become harder to read, harder to fix, and slower to run as they grow. A choice that works for ten items can become genuinely painful with ten thousand. A structure that stores data correctly can still make your code confusing if it hides the meaning of what you are tracking.
Good programmers treat data design as a real step in solving a problem. Before writing code, they ask a short set of questions:
This chapter gives you a framework for answering these questions.
None of the structures in this chapter are new. The skill being built here is judgment — the habit of comparing options deliberately instead of defaulting to whichever structure you used last time.
Before comparing structures, review what each one is.
A list is an ordered, mutable sequence. You can store any mix of values, access them by index, add items, remove items, and change items in place.
scores = [88, 72, 95, 61, 84] scores.append(77) scores[0] = 90
Lists are the most general-purpose collection. When you are not sure which structure to use, you will often reach for a list first.
A tuple is an ordered, immutable sequence. Once created, it cannot be changed.
dimensions = (1920, 1080) point = (3.5, -2.1)
Tuples signal that a group of values belongs together as a fixed unit. They are useful for coordinates, settings, records returned from a function, and any case where you want Python to prevent accidental modification.
A set is an unordered collection of unique values. Duplicates are automatically discarded. Sets support fast membership testing and set operations like union and intersection.
seen_ids = {1001, 1002, 1003}
seen_ids.add(1004)
seen_ids.add(1001) # Already there; set does not change
print(1002 in seen_ids) # True
Sets are useful when you care about whether something exists, not where it is or how many times it appears.
A dictionary is an unordered collection of key-value pairs. You look up values by key, not by position. Keys must be unique and immutable.
student = {
"name": "Amara",
"grade": 10,
"gpa": 3.7
}
print(student["name"])
Dictionaries are the natural structure for named records, lookup tables, and counting problems. They are the most powerful general-purpose mapping structure in Python.
One of the first questions to ask about your data is whether order matters.
For ordered data, use a list or a tuple. Lists are for ordered data that changes. Tuples are for ordered data that should stay fixed.
For unordered data, use a set (if you only care about membership) or a dictionary (if each item has an associated value).
Many beginners store unordered data in lists and then write slow, complicated code to search through them.
submitted_ids = [1001, 1002, 1003, 1004]
if 1002 in submitted_ids:
print("Already submitted")
submitted_ids = {1001, 1002, 1003, 1004}
if 1002 in submitted_ids:
print("Already submitted")
When you only need to know whether something is present and you never need to know where it falls in a sequence, a set is a better fit than a list.
Ask yourself: does this data allow duplicates, or must every value appear at most once?
A list of quiz scores for a class allows repeated values. Two students can both score 85. The duplicate values carry real information — two different students earned that score.
quiz_scores = [85, 72, 85, 90, 61, 72, 85]
Here a list is correct. A set would silently discard the duplicates, losing information.
A collection of registered usernames should not allow the same username twice. A set of unique items sold today should not count the same item more than once in its membership.
registered_usernames = {"amara2024", "bento_k", "chen_li"}
A set enforces uniqueness automatically. You do not have to write extra code to prevent duplicates.
Sometimes duplicates carry information not as individual entries, but as counts.
word_counts = {}
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
for word in words:
if word in word_counts:
word_counts[word] += 1
else:
word_counts[word] = 1
print(word_counts)
# {'apple': 3, 'banana': 2, 'cherry': 1}
When duplicates are meaningful as frequencies, a dictionary with counts is the right structure.
A second major axis of comparison is how you access the data.
Lists and tuples let you access items by their index. Position has meaning. The first item is different from the third item because of where it sits.
temperatures = [72.1, 68.4, 75.0, 80.2, 79.9] print(temperatures[0]) # First reading print(temperatures[-1]) # Most recent reading
This works well when items are naturally ordered and position carries meaning: days of the week, steps in a process, readings taken in sequence.
Dictionaries let you access items by a meaningful name. You do not need to remember that the student's GPA is stored at index 2. You look it up by the key "gpa".
student = {"name": "Amara", "grade": 10, "gpa": 3.7}
print(student["gpa"])
Key-based access is clearer whenever the data has named fields. Compare:
student = ("Amara", 10, 3.7)
print(student[2]) # What is index 2? You have to remember.
student = {"name": "Amara", "grade": 10, "gpa": 3.7}
print(student["gpa"]) # Clear
When names convey meaning more clearly than positions, use a dictionary.
Mutable data can be changed after it is created. Immutable data cannot.
| Structure | Mutable? |
|---|---|
| List | Yes |
| Dictionary | Yes |
| Set | Yes |
| Tuple | No |
Tuples are not just a less-capable version of lists. They communicate something: these values belong together and should not change.
A coordinate pair like (latitude, longitude) should be a tuple. You are not going to modify one component of a location mid-program. The pair is a single unit of fixed information.
An RGB color like (255, 128, 0) should be a tuple. Colors in a design palette do not change at runtime.
Configuration settings loaded once at startup can reasonably be stored in a tuple to prevent accidental modification.
WINDOW_SIZE = (1280, 720) # Tuple: this should not change during the program player_position = [640, 360] # List: this changes constantly
Using tuples for fixed data and lists for changing data makes your intent visible in the code itself.
Many programming problems involve tracking records: things with multiple named properties. A student has a name, a grade level, and a GPA. A product has a name, a price, and a quantity in stock. A contact has a first name, a last name, a phone number, and an email address.
Python gives you two main ways to model a single record before you reach classes: a tuple or a dictionary.
student = ("Amara", 10, 3.7)
This works, but the meaning of each position is not visible in the code. You must remember the order: name first, grade second, GPA third. This is fragile. If you later add a field in the middle, every piece of code that unpacks the tuple may break.
Tuple records work best when the structure is small, stable, and well-known — like a coordinate or a date.
student = {
"name": "Amara",
"grade": 10,
"gpa": 3.7
}
A dictionary record is more readable. The keys document themselves. You can add a new field without breaking existing code that reads other fields. The cost is slightly more typing.
For most beginner-to-intermediate programs, dictionary records are clearer and safer than tuple records unless the structure is very simple.
Real programs rarely use one structure in isolation. They combine them. The most common combinations are described below.
students = [
{"name": "Amara", "grade": 10, "gpa": 3.7},
{"name": "Bento", "grade": 11, "gpa": 3.2},
{"name": "Chen Li", "grade": 10, "gpa": 3.9}
]
for student in students:
print(student["name"], student["gpa"])
This models a table where each row is a dictionary. It is one of the most useful patterns in practical Python.
class_roster = {
"grade_10": ["Amara", "Chen Li", "Fatima"],
"grade_11": ["Bento", "Kenji", "Lucia"]
}
print(class_roster["grade_10"])
This models groups. Each key names a category and maps to the list of items in that category.
inventory = {
"apple": {"price": 0.99, "quantity": 150},
"banana": {"price": 0.59, "quantity": 80},
"cherry": {"price": 3.49, "quantity": 40}
}
print(inventory["banana"]["price"])
This models a lookup table where each entry has multiple named properties.
data tells you nothing. A variable named students, inventory, or word_counts tells you what it contains and makes the structure more readable.No structure is best for everything. Every choice involves tradeoffs.
| Need | Best fit | Why |
|---|---|---|
| Ordered sequence that changes | List | Mutable, indexed, flexible |
| Ordered sequence that should not change | Tuple | Immutable, signals fixed data |
| Unique values, fast membership check | Set | No duplicates, fast in check |
| Named fields for a single record | Dictionary | Keys are self-documenting |
| Counting occurrences | Dictionary with integer values | Natural accumulator pattern |
| Table of similar records | List of dictionaries | Rows of uniform shape |
| Groups of items by category | Dictionary of lists | Category as key, items as list |
| Fast lookup by name | Dictionary | O(1) average key lookup |
| Preserving insertion order | List or dictionary (Python 3.7+) | Both preserve insertion order |
| Removing duplicates from a sequence | Set | Automatic deduplication |
For most beginner Python programs, you will use lists and dictionaries for nearly everything. Lists handle sequences. Dictionaries handle records and lookups. Sets and tuples are valuable but more specialized.
The skill being built in this chapter is not memorizing a formula. It is the habit of pausing before you write code and asking: what shape does this data want to have?
One of the best habits a programmer can develop is sketching data structures on paper — or in a comment at the top of a file — before writing any code.
This sketch does not need to be formal. It just needs to answer: what does one piece of this data look like? What does a collection of it look like?
Suppose you are building a contact book that stores names, phone numbers, and email addresses. You want to look up a contact by name.
contacts = [("Amara Johnson", "555-1234", "[email protected]"), ("Bento Kowalski", "555-5678", "[email protected]")]contacts = [{"name": "Amara Johnson", "phone": "555-1234", "email": "[email protected]"}, {"name": "Bento Kowalski", "phone": "555-5678", "email": "[email protected]"}]contacts = {"Amara Johnson": {"phone": "555-1234", "email": "[email protected]"}, "Bento Kowalski": {"phone": "555-5678", "email": "[email protected]"}}print(contacts["Amara Johnson"]["phone"])You would arrive at Sketch 3 faster if you asked the right questions before writing code:
The sketch is not the program. It is the plan that makes the program obvious.
Sometimes you build a program with one structure and later discover it is the wrong choice. This is normal. The ability to recognize a weak design and improve it is a real programming skill.
if item in list. A set would be more appropriate.names = ["Amara", "Bento", "Chen Li"] scores = [88, 72, 95] grades = ["B", "C", "A"]
These three lists are coupled. If you sort scores, names and grades go out of sync unless you are careful.
students = [
{"name": "Amara", "score": 88, "grade": "B"},
{"name": "Bento", "score": 72, "grade": "C"},
{"name": "Chen Li", "score": 95, "grade": "A"}
]
Now each student's data travels together. Sorting by score keeps name and grade attached to the correct student.
product_names = ["apple", "banana", "cherry"]
product_prices = [0.99, 0.59, 3.49]
# To find the price of banana:
index = product_names.index("banana")
price = product_prices[index]
prices = {"apple": 0.99, "banana": 0.59, "cherry": 3.49}
price = prices["banana"]
The second version is shorter, clearer, and faster.
In this chapter you learned how to evaluate data structure choices rather than simply accepting the first structure that comes to mind.
Read each snippet carefully. Answer the questions without running the code.
seen = set()
items = ["apple", "banana", "apple", "cherry", "banana", "apple"]
for item in items:
seen.add(item)
print(len(seen))
seen contain after the loop?len(seen) print?len(items)?catalog = {
"pen": {"price": 1.25, "stock": 200},
"notebook": {"price": 4.50, "stock": 85},
"ruler": {"price": 0.99, "stock": 300}
}
for item, details in catalog.items():
if details["stock"] < 100:
print(item, "is low on stock")
records = [
{"name": "Amara", "score": 88},
{"name": "Bento", "score": 72},
{"name": "Chen Li", "score": 95}
]
total = 0
for r in records:
total += r["score"]
average = total / len(records)
print(f"Average score: {average:.1f}")
records were an empty list? Would the program crash, or produce a wrong answer?inventory = {}
items = [("apple", 5), ("banana", 3), ("apple", 2), ("cherry", 7), ("banana", 1)]
for name, qty in items:
if name in inventory:
inventory[name] += qty
else:
inventory[name] = qty
print(inventory)
| After iteration | inventory |
|---|---|
| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 |
What does the program print?
groups = {}
students = [
("Amara", "A"),
("Bento", "B"),
("Chen Li", "A"),
("Fatima", "C"),
("Kenji", "B"),
("Lucia", "A")
]
for name, grade in students:
if grade not in groups:
groups[grade] = []
groups[grade].append(name)
for grade in sorted(groups):
print(grade, groups[grade])
Trace the state of groups after each iteration of the first loop. Then write exactly what the program prints.
Predict what each snippet prints, or describe what error it produces.
colors = {"red", "blue", "green", "red", "blue"}
print(len(colors))
print("purple" in colors)
point = (4, 7) point[0] = 10
data = {"x": [1, 2, 3], "y": [4, 5, 6]}
data["x"].append(99)
print(data)
words = ["the", "quick", "the", "brown", "fox", "the"] unique = set(words) print(sorted(unique))
Each program below contains a problem. Identify the error and explain how to fix it.
# Goal: store each student's scores records = {} records["Amara"] = 88 records["Bento"] = 72 records["Amara"] = 95 # Amara took the quiz again print(records["Amara"]) # Should print both 88 and 95
What is wrong with this design? What structure would let you store multiple scores per student?
contacts = [
("Amara", "555-1234"),
("Bento", "555-5678")
]
print(contacts["Amara"])
What error does this produce? Explain why, and suggest a better data structure for this use case.
tags = ["python", "beginner", "python", "tutorial", "beginner"]
unique_tags = set(tags)
unique_tags[0] # Get the first tag
What is wrong with this code? What should you do instead if you need an ordered, deduplicated list of tags?
students = [
{"name": "Amara", "score": 88},
{"name": "Bento", "score": 72},
{"name": "Chen Li"} # Missing "score" key
]
for s in students:
print(s["name"], s["score"])
What error will this produce and when? How would you guard against it?
For each situation, state which data structure you would use and explain why in one or two sentences.
You are building a gradebook for a class of students. Each student has a first name and last name, a student ID number, a list of assignment scores, and a final letter grade.
You are building an inventory system for a small store. The store sells several products. Each product has a name, a price, and a quantity on hand. You need to look up a product's price by name, reduce the quantity when a product is sold, and print a report of all products with fewer than 10 units remaining.
names = ["Amara", "Bento", "Chen Li", "Fatima"]
scores = [88, 72, 95, 61]
passing = [True, True, True, False]
for i in range(len(names)):
if passing[i]:
print(names[i], "passed with", scores[i])
else:
print(names[i], "did not pass")
This program works correctly. Identify at least two design improvements and explain each one.
# Each contact stored as: (first_name, last_name, phone, email, city) contacts = [ ("Amara", "Johnson", "555-1234", "[email protected]", "Springfield"), ("Bento", "Kowalski", "555-5678", "[email protected]", "Riverside") ] # Print each contact's email for c in contacts: print(c[3])
Identify what is fragile about the data design and suggest a safer alternative.
Write a function called count_votes that takes a list of strings representing votes (for example ["yes", "no", "yes", "yes", "no", "abstain"]) and returns a dictionary mapping each option to its vote count.
def count_votes(votes):
# Your code here
pass
results = count_votes(["yes", "no", "yes", "yes", "no", "abstain"])
print(results)
# Expected: {'yes': 3, 'no': 2, 'abstain': 1}
Write a function called group_by_grade that takes a list of student dictionaries (each with "name" and "grade" keys) and returns a dictionary mapping each grade to a list of student names.
students = [
{"name": "Amara", "grade": "A"},
{"name": "Bento", "grade": "B"},
{"name": "Chen Li", "grade": "A"},
{"name": "Fatima", "grade": "C"},
{"name": "Kenji", "grade": "B"}
]
def group_by_grade(students):
# Your code here
pass
print(group_by_grade(students))
# Expected: {'A': ['Amara', 'Chen Li'], 'B': ['Bento', 'Kenji'], 'C': ['Fatima']}
Write a function called remove_duplicates that takes a list and returns a new list with duplicates removed, preserving the original order of first appearances.
def remove_duplicates(items):
# Your code here
pass
print(remove_duplicates(["apple", "banana", "apple", "cherry", "banana"]))
# Expected: ['apple', 'banana', 'cherry']
A set can track which items you have already seen.
Refactor the following program to use a list of dictionaries instead of parallel lists.
product_names = ["pen", "notebook", "ruler", "eraser"]
product_prices = [1.25, 4.50, 0.99, 0.75]
product_stock = [200, 85, 300, 150]
for i in range(len(product_names)):
print(product_names[i], "$" + str(product_prices[i]), "qty:", product_stock[i])
After refactoring, add a loop that prints only the products priced under $2.00.
Write a function called find_common that takes two lists and returns a set of values that appear in both lists.
def find_common(list_a, list_b):
# Your code here
pass
a = [1, 2, 3, 4, 5]
b = [3, 4, 5, 6, 7]
print(find_common(a, b))
# Expected: {3, 4, 5}
These questions draw on material from Chapters 16 through 20.
sorted({"banana", "apple", "cherry"}) return? What type does it return?list.append(item) and list.extend(other_list)?dict.get("key", default) do that dict["key"] does not?"mississippi".for loop? What does the loop variable contain?inventory["apple"] only if "apple" is already a key in inventory. Do not raise a KeyError if it is not present.Build a console-based inventory tracker that lets the user manage a small store's stock. The program should store inventory as a dictionary of dictionaries.
inventory = {
"apple": {"price": 0.99, "quantity": 150},
"banana": {"price": 0.59, "quantity": 80},
"cherry": {"price": 3.49, "quantity": 8}
}
def add_product(inventory, name, price, quantity):
pass
def restock(inventory, name, amount):
pass
def sell(inventory, name, amount):
pass
def view_inventory(inventory):
pass
def low_stock_report(inventory, threshold=10):
pass
def main():
inventory = {}
running = True
while running:
print("\n--- Inventory Tracker ---")
print("1. Add product")
print("2. Restock product")
print("3. Sell product")
print("4. View all inventory")
print("5. Low stock report")
print("6. Quit")
choice = input("Choice: ")
# Your menu logic here
main()
"name" key and a "score" key. You want to find the dictionary with the highest score. Write the logic, in plain English, for how you would do this. Then write it in Python.contacts = {
"Amara Johnson": {"phone": "555-1234", "email": "[email protected]"},
"Bento Kowalski": {"phone": "555-5678", "email": "[email protected]"},
"Chen Li": {"phone": "555-9012", "email": "[email protected]"}
}
Chapter 20 completes Part IV by teaching you to choose and combine data structures deliberately. Chapter 21 begins Part V: Files, Data, and Practical Automation. So far, every program you have written starts fresh when it runs and forgets everything when it ends. Files change that. Chapter 21 will teach you how to read data from text files, write data back to files, append to existing files, and handle the file path and file error problems that come with working outside the program's memory. You will build programs that can remember information between sessions — a skill that makes programs far more useful in the real world.