Python course Part 3: The Ultimate Guide to Python Data Structures

In Parts 1 and 2 we stored single pieces of data — a secret_passcode, a battery_level — and made decisions based on them.
But what happens when you need to store 10,000 compromised IP addresses? Or the health, stats, and location of 50 different enemies in a game? Creating 50 separate variables (enemy_1_health, enemy_2_health, …) is a one-way ticket to madness.
Today we learn how to group and organize data. By the end of this post we'll have built a text-adventure inventory system that tracks items, prevents duplicates, and manages quantities.
Here's the target:
--- Player Inventory ---
Health Potions: 3
Gold Coins: 150
Unique Items: {'Rusty Key', 'Map'}
[Action] You looted a chest! Found: ['Iron Shield', 'Health Potion', 'Map', 'Health Potion']
--- Updating Inventory ---
[+] Health Potions increased to 4.
[!] You already have a Map. Discarding duplicate.
[+] Added Iron Shield.
[+] Health Potions increased to 5.
Let's meet the core four: Lists, Tuples, Sets, and Dictionaries.
1. Lists — The Workhorse
A list is exactly what it sounds like: an ordered collection of items. You make one with square brackets []. Lists are mutable — you can add, remove, and change items after creation.
loot_drops = ["Gold", "Health Potion", "Iron Sword"]
# Access by index — Python is zero-indexed, so the first item is [0]:
print(loot_drops[0]) # Gold
print(loot_drops[-1]) # Iron Sword (negative indexes count from the end)
# Modify the list in place:
loot_drops.append("Magic Ring") # add to the end
loot_drops.remove("Iron Sword") # remove a specific item by value
loot_drops[1] = "Major Health Potion" # replace the item at index 1
Lists are your default choice for ordered data: a queue of tasks, a log of recent actions, a sequence of results.
2. Tuples — The Unchangeable List
A tuple looks and behaves a lot like a list, but it uses parentheses () and is immutable. Once created, you cannot add, remove, or change anything inside it.
server_coordinates = (192, 168, 1, 15)
print(server_coordinates[0]) # 192 — reading is fine
server_coordinates[0] = 10 # 💥 TypeError — this line crashes the program
Why would you want something you can't change? Two reasons. Immutability makes tuples slightly faster and lighter than lists, and — more importantly — it's a guarantee. When you hand someone a tuple, you're promising the data won't shift under their feet. Use them for fixed records: coordinates, RGB colors, a row from a database, config that shouldn't be touched at runtime.
3. Sets — The Bouncer
A set uses curly braces {} and has one defining superpower: it refuses duplicates. Try to add something that's already there and the set silently ignores you. Sets are also unordered — don't rely on the items staying in any particular sequence.
visited_locations = {"Tavern", "Dark Forest", "Castle"}
visited_locations.add("Tavern") # already present — silently ignored
print(visited_locations)
# {'Castle', 'Tavern', 'Dark Forest'} — note the order is not what we typed
That makes sets the perfect tool for de-duplicating data or tracking "have I seen this before?"
🛑 Dev Callout: Algorithmic Complexity (Big O)
Coming from C++ or Java? Here's the one that actually matters for performance.
If you need to check whether an item exists in a large collection —
if item in collection:— do not use a list. A list has to scan every element one by one, anO(N)linear search.A Python set is a hash table under the hood, so membership checks are
O(1)— constant time, regardless of size. If you're parsing logs against a blacklist of 500,000 malicious IPs, loading them into a set instead of a list turns a crawl into something instant. Same idea powers the halving strategy from Part 2's guessing game: cutting the search space in half each step isO(log N), which is why 5 guesses is enough for 100 numbers.
4. Dictionaries — The Key-Value Store
If you master only one data structure in Python, make it the dictionary (dict). Dictionaries store data as key → value pairs: you look up a key to get its value. They use curly braces {} like sets, but with a colon : between each key and value.
player_stats = {
"name": "Arthur",
"level": 5,
"class": "Knight",
}
# Look up by key:
print(player_stats["class"]) # Knight
# Add or update:
player_stats["level"] = 6 # key exists → updates it
player_stats["guild"] = "The Ravens" # key is new → creates it
Dictionaries are powerful because a value can be anything — a number, a string, a list, even another dictionary. That's how you model complex, nested data without inventing a class for every little thing. (Coming from C#/Java, this is your Dictionary<TKey, TValue> / HashMap — same hash-table guarantees, far less ceremony.)
5. Bringing It Together: The Inventory System
Let's combine these to build the inventory. We'll use:
a dictionary for stackable items where the quantity matters (potions, gold), and
a set for unique items the player can only hold one of (a map, a quest key).
Create a file named inventory.py:
# 1. Our two data structures
inventory_counts = { # stackable items: name -> how many
"Health Potions": 3,
"Gold Coins": 150,
}
unique_items = {"Rusty Key", "Map"} # one-of-a-kind items
# 2. A chest full of new loot (a plain list of what we found)
new_loot = ["Iron Shield", "Health Potion", "Map", "Health Potion"]
print("--- Player Inventory ---")
# .items() lets us loop over both the key and the value at once:
for item, count in inventory_counts.items():
print(f"{item}: {count}")
print(f"Unique Items: {unique_items}\n")
print(f"[Action] You looted a chest! Found: {new_loot}\n")
print("--- Updating Inventory ---")
# 3. Process each item we found
for item in new_loot:
if item == "Health Potion":
inventory_counts["Health Potions"] += 1
print(f"[+] Health Potions increased to {inventory_counts['Health Potions']}.")
elif item == "Gold Coin":
inventory_counts["Gold Coins"] += 1
print("[+] Added a Gold Coin.")
else:
# Treat anything else as a unique item — let the set enforce uniqueness:
if item in unique_items:
print(f"[!] You already have a {item}. Discarding duplicate.")
else:
unique_items.add(item)
print(f"[+] Added {item}.")
# 4. Final state
print("\n--- Final Inventory ---")
print(f"Quantities: {inventory_counts}")
print(f"Unique Items: {unique_items}")
Run it, then try editing new_loot to throw different items at it. Notice how the set quietly handles the duplicate Map while the dictionary happily stacks the potions.
What's Next?
You can now organize and reshape data. But our script is getting long, and the item-adding logic is hardcoded right inside the loop. What if the player could also sell, drop, or trade items? Copy-pasting that for loop everywhere would be a nightmare to maintain.
In Part 4: Functions & Scope, we'll wrap our logic into clean, reusable blocks — the step that turns a script into actual software.