Python Dictionaries: Key-Value Storage Explained

If lists are Python's ordered collection, dictionaries are its labeled one. A dictionary maps keys to values, letting you look up any value instantly by name instead of by position. This makes dicts the ideal structure for structured records β€” a crop's stats, a player's profile, a configuration object. In this guide you'll learn to create, access, update, iterate, and compose dictionaries.

What Is a Dictionary?

Dictionaries store pairs of keys and values. Each key must be unique and immutable (strings and integers are the most common choices). Values can be anything β€” numbers, strings, lists, or even other dicts.

Key property: Dictionary lookups are O(1) β€” they take the same time whether the dict has 10 entries or 10,000. This is what makes dicts so powerful for fast lookups.

Creating a Dictionary

Use curly braces with key: value pairs, or call dict() with keyword arguments.

python
# Dict literal
wheat = {
    "name": "wheat",
    "cost": 10,
    "value": 25,
    "days": 5,
    "mature": False,
}

# dict() constructor
carrot = dict(name="carrot", cost=5, value=15, days=3, mature=False)

# Empty dict
player = {}
print(type(player))   # <class 'dict'>
print(len(wheat))     # 5  β€” number of key-value pairs

Accessing Values

Use square-bracket notation to get a value by key. If the key doesn't exist, Python raises a KeyError. Use .get() to safely return a default instead.

python
crop = {"name": "tomato", "cost": 15, "value": 40, "days": 7}

# Direct access β€” raises KeyError if key missing
print(crop["name"])    # tomato
print(crop["value"])   # 40

# .get() β€” returns None by default, or your chosen fallback
print(crop.get("water"))           # None
print(crop.get("water", 0.5))     # 0.5  (default)
print(crop.get("name", "unknown")) # tomato

Adding and Updating Entries

Assign to a key to add it if new, or update it if it already exists. Use .update() to merge another dict or keyword arguments in one call.

python
player = {"name": "Alex", "gold": 100, "level": 1}

# Add a new key
player["plots"] = 4

# Update an existing key
player["gold"] += 50
print(player["gold"])   # 150

# Merge with .update()
player.update({"level": 2, "xp": 500})
print(player)
# {'name': 'Alex', 'gold': 150, 'level': 2, 'plots': 4, 'xp': 500}

Removing Entries

Three ways to remove items: .pop(key) removes and returns a value, del dict[key] removes in place, and .clear() empties the entire dict.

python
inventory = {"wheat": 3, "carrot": 5, "tomato": 2, "pumpkin": 0}

# .pop() β€” remove and get the value
count = inventory.pop("tomato")
print(count)       # 2
print(inventory)   # {'wheat': 3, 'carrot': 5, 'pumpkin': 0}

# .pop() with a default (no KeyError if missing)
pumpkin = inventory.pop("pumpkin", 0)
print(pumpkin)     # 0

# del β€” in-place removal
del inventory["carrot"]
print(inventory)   # {'wheat': 3}

# .clear() β€” empty the dict
inventory.clear()
print(inventory)   # {}

Checking Keys

Use the in operator to test whether a key exists. This is O(1) β€” extremely fast even for large dicts.

python
crop_db = {"wheat": 10, "carrot": 5, "tomato": 15}

print("wheat" in crop_db)      # True
print("pumpkin" in crop_db)    # False
print("pumpkin" not in crop_db)  # True

# Safe access pattern
crop = "tomato"
if crop in crop_db:
    print(f"{crop} costs {crop_db[crop]} gold")

Iterating Over a Dictionary

Three iteration methods cover every case: .keys(), .values(), and .items(). Use .items() when you need both.

python
crops = {
    "wheat":  {"cost": 10, "value": 25},
    "carrot": {"cost": 5,  "value": 15},
    "tomato": {"cost": 15, "value": 40},
}

# Just keys
for name in crops.keys():
    print(name)        # wheat, carrot, tomato

# Just values
for stats in crops.values():
    print(stats["value"])

# Keys AND values β€” the most common pattern
for name, stats in crops.items():
    profit = stats["value"] - stats["cost"]
    roi = (profit / stats["cost"]) * 100
    print(f"{name}: profit {profit} gold, ROI {roi:.0f}%")

Nested Dictionaries

Dictionaries can hold other dictionaries as values, making them ideal for hierarchical data like a full farm state.

python
farm = {
    "player": {"name": "Alex", "gold": 200, "level": 3},
    "season": {"name": "Spring", "day": 7, "length": 28},
    "plots": {
        (0, 0): {"crop": "wheat",  "days": 4, "mature": False},
        (1, 0): {"crop": "carrot", "days": 3, "mature": True},
    }
}

# Access nested values
print(farm["player"]["gold"])          # 200
print(farm["plots"][(1, 0)]["mature"]) # True

# Update a nested value
farm["player"]["gold"] -= 50
farm["season"]["day"] += 1

Dict Comprehensions

Just like list comprehensions, you can build a dictionary in a single expression using {key: value for item in iterable}.

python
crop_list = [
    {"name": "wheat",  "cost": 10, "value": 25},
    {"name": "carrot", "cost": 5,  "value": 15},
    {"name": "tomato", "cost": 15, "value": 40},
]

# Build a dict: name -> profit
profit_map = {
    c["name"]: c["value"] - c["cost"]
    for c in crop_list
}
print(profit_map)
# {'wheat': 15, 'carrot': 10, 'tomato': 25}

# Filter to only profitable crops (profit > 12)
top_crops = {k: v for k, v in profit_map.items() if v > 12}
print(top_crops)   # {'wheat': 15, 'tomato': 25}

Practical: Crop Stats as a Dict

Here's how GrowBit might store and query crop data using nested dicts β€” a common real-world pattern in game configs and APIs alike.

python
CROP_DB = {
    "wheat":   {"name": "wheat",   "cost": 10, "value": 25, "days": 5},
    "carrot":  {"name": "carrot",  "cost": 5,  "value": 15, "days": 3},
    "tomato":  {"name": "tomato",  "cost": 15, "value": 40, "days": 7},
    "pumpkin": {"name": "pumpkin", "cost": 20, "value": 60, "days": 10},
}

def best_crop_by_roi(db):
    """Return the crop name with the highest return on investment."""
    return max(
        db,
        key=lambda name: (db[name]["value"] - db[name]["cost"]) / db[name]["cost"]
    )

print(best_crop_by_roi(CROP_DB))   # pumpkin  (ROI: 200%)