Technology Python Dictionaries & Hash Maps Tutorial

Learn with diagrams, code, systems and practical examples.

</>
Python tutorial · Learn → Build → Test

Python dictionaries: stop memorizing methods and learn the mapping model.

A dictionary is not “a faster list.” It is a model for answering a different question: given this key, what value belongs to it? Build that mental model first, then missing-key handling, counting, grouping, caching, and interview problems become much easier to reason about.

1 · First principles

A dictionary is a lookup contract, not a bag of syntax

Imagine a checkout system that receives the SKU A17 and needs the price. A list can hold prices, but a list asks you to know a position: “which item is at index 12?” The business question is different: “what price belongs to SKU A17?” That is a mapping. A Python dictionary expresses the relationship directly:

prices = {
    "A17": 9.99,
    "B04": 14.50,
}

price = prices["A17"]
Dictionary lookup mental modelA key such as A17 passes through hash and equality lookup to retrieve its associated value 9.99. The diagram contrasts key-based lookup with positional indexing.Key"A17"Hash + equalityfind the matching keynot “item at index N”Value9.99
Original concept diagram: dictionaries answer “value by key.” The implementation may use hashing internally, but your design job is to choose a stable lookup identity and a useful associated value.

Think of the dictionary as maintaining pairs of unique keys and associated values. The key is the lookup identity. The value is the information you want once that identity is known. Python’s current documentation describes dictionaries as mappings from hashable values to arbitrary objects. That word hashable is important: keys need a stable hash/equality identity. Strings, integers, and tuples of hashable values commonly work; mutable lists and dictionaries do not.

Before writing code, ask one question: what do I need to retrieve by what? If the answer is “price by SKU,” “profile by user ID,” or “count by category,” a dictionary is a natural candidate. If the answer is “items in sequence, including duplicates,” a list may fit better. If the answer is “have I seen this value?” with no associated data, a set may be clearer.

Predict before running: If two assignments use the same key, how many values can the dictionary expose through that key?
Reveal the reasoning

One. Assigning the same key again replaces the associated value. The dictionary models one current value per key, so duplicate keys are not separate positions like duplicate list items.

2 · Keys

Hashability protects the lookup identity

Why can (user_id, report_date) be a key while [user_id, report_date] cannot? The tuple can be hashable when its contents are hashable; the list is mutable. If a key could change in a way that changed its hash while it lived inside the mapping, the dictionary could no longer reliably find the bucket associated with that key. You do not need to memorize the internal table layout to use this rule well. You only need the design principle: a key’s lookup identity must stay stable while it is used as a key.

This makes composite keys useful for local caches and indexes:

report_cache = {}
key = (user_id, report_date)
report_cache[key] = generated_report

But do not use a tuple merely because you can. A composite key is useful when the pair itself is the identity. If your data has richer invariants, a small domain object or database model may communicate more clearly.

3 · Lookup

Presence, value, and truthiness are three different questions

One of the most common dictionary bugs happens when code confuses “is the key present?” with “is the stored value truthy?”. Consider a cache that stores an index:

index_by_id = {"first": 0}

if index_by_id.get("first"):
    print("found")

This does not print because 0 is falsy. The key is present; the conditional asked the wrong question. If presence matters, write "first" in index_by_id. If the actual value matters, retrieve it and compare using the domain’s real rules.

Membership with in checks dictionary keys. That is exactly what you want for many deduplication, cache, and indexing decisions. If you actually need to search values, say so explicitly with d.values()—and recognize that “find a key by searching all values” may indicate that your mapping is oriented in the wrong direction.

4 · Missing keys

Choose whether absence is an error, an expected state, or a state transition

Python gives you multiple missing-key tools because “missing” can mean different things. d[key] raises KeyError when the key does not exist. That is useful when absence violates an invariant. If every order must have a known customer record, silently replacing a missing customer with None may hide corrupted data.

d.get(key, default) is better when absence is expected and has a meaningful read-only fallback. It does not insert the fallback:

count = counts.get("red", 0)  # counts is unchanged if red is missing

setdefault solves a different problem: it can insert a default and return the stored value. That makes it useful for simple grouping:

groups.setdefault(category, []).append(item)

A subtle bug is groups.get(category, []).append(item). When the key is missing, get returns a temporary list. You append to that list, then throw it away. Nothing was inserted into the dictionary.

Another trap is using None as the missing signal when None is a legitimate stored value. In that case use membership or a unique sentinel object. Your code should preserve the distinction between missing and present with a value that happens to be falsey.

5 · Mutation & order

Insertion order is guaranteed—but it is not sorting

Current Python guarantees dictionary insertion order. Updating an existing key changes its value without moving the key to a new position. Deleting a key and inserting it later is a new insertion, so it returns at the end. This is useful, but it solves a specific requirement: preserving insertion history. It does not mean a dictionary is alphabetically sorted.

If a report must be ordered by SKU, make sorting explicit:

for sku in sorted(prices):
    print(sku, prices[sku])

Dictionary views from keys(), values(), and items() are dynamic views. If you keep a view and later mutate the mapping, the view reflects the mapping’s current state. That is powerful, but structural mutation while directly iterating can make code fragile or raise an error. When you need to delete keys based on a scan, iterate over a separate list of candidate keys or collect the changes first.

6 · Mutable values

Lookup returns the stored object—it does not secretly clone it

A dictionary can store any Python object as a value, including mutable lists and dictionaries. That means aliases matter:

groups = {"admin": ["Ada"]}
admins = groups["admin"]
admins.append("Lin")

print(groups["admin"])  # ['Ada', 'Lin']

admins and groups["admin"] point to the same list. This is normal Python object behavior, not a dictionary bug. The useful debugging question is: did this operation create a new object, or mutate the object already stored?

Nested structures can become hard to reason about when every caller knows five layers of keys and lists. Dictionaries are excellent flexible tools, but flexibility can become hidden coupling. If a structure has stable domain rules, a dataclass, TypedDict, dedicated class, or focused helper API may make those rules easier to test and maintain.

7 · Counting

Counting is a mapping from item → frequency

A frequency table is one of the clearest dictionary use cases. The state says, “for this item, how many times have I seen it?”

counts = {}
for item in events:
    counts[item] = counts.get(item, 0) + 1

This is worth tracing by hand once. For each event: look up the old count (or zero), add one, and assign the new value. When the task is specifically counting hashable objects, collections.Counter expresses the intent more directly and gives counting-oriented operations. The design skill is not “always use Counter.” It is recognizing the underlying invariant so you can choose the clearest tool.

8 · Grouping

Grouping maps a key to a collection—but define who owns that collection

Grouping products by category produces a mapping from category → list of products. setdefault is enough for small examples. For larger codebases you may prefer defaultdict(list) or an explicit branch because those choices make the missing-key behavior visible in different ways.

The deeper question is ownership. If callers receive the stored list and mutate it, they mutate the group. Sometimes that is intentional; sometimes a function should return a copy or a read-only representation. The dictionary does not decide your domain boundary for you.

9 · Design decisions

Use dictionaries to make a question cheap and explicit—not to make every problem “O(1)”

Hash-based mappings typically provide efficient expected-time lookup, but architecture decisions should not be reduced to a slogan that “dictionary = constant time.” Hash collisions, resizing, memory, key computation, and surrounding work still exist. More importantly, the right structure is the one that represents the required operation and invariant.

The Two Sum exercise stores prior values mapped to indices so each new number can ask whether its complement has been seen. The Subarray Sum problem goes one step further: it stores prefix sums mapped to frequencies, because the same prior prefix can represent multiple valid subarrays. Both are “hash-map problems,” but the stored state means something different. If you can explain that meaning, you understand the algorithm. If you only remember “use a dict,” you do not.

Guided practice: Design an in-process cache keyed by (user_id, report_date). What happens when the database row changes?
Reveal a strong answer

The tuple is a reasonable composite key if both parts are hashable, but the dictionary is only a cache—not the source of truth. Define expiry or invalidation, decide what a miss means, and decide whether stale data is acceptable. The dictionary solves lookup; it does not solve cache consistency.

10 · Common mistakes

Five mistakes worth recognizing on sight

  1. Truthiness instead of presence: if d.get(key) fails for valid values such as 0 or False.
  2. Using get as if it inserts: a fallback returned by get is not automatically stored.
  3. Assuming insertion order is sorted order: sort explicitly when sorting is the requirement.
  4. Mutating shared nested values unintentionally: names retrieved from a dictionary can alias the same mutable object.
  5. Using a dictionary when a set or list better communicates the invariant: data structure choice should make the business question obvious.
11 · Next steps

Turn the mental model into practice

Use the existing Python Data Structures lesson when you want the broader list/tuple/set/dictionary comparison. Then apply the mapping model in Two Sum and Subarray Sum. After that, explain your decisions aloud in the interview drills and take the timed Online Test.

Authoritative references

Current Python documentation used for this tutorial