PYTHON / DICTIONARIES AND SETS
Creating dictionaries and the rules for keys
Build dictionaries with literals, dict(), zip, and fromkeys, and predict exactly which key objects survive.
What you will learn
- Pick between {} literals, dict(pairs), dict(**kwargs), and dict(zip(a, b))
- Explain why lists fail as keys but tuples and strings work
- Predict that 1, 1.0 and True collapse into one key slot
- Use dict.fromkeys safely and avoid its shared mutable default value
Understanding Creating dictionaries and the rules for keys
A dictionary maps keys to values by computing hash(key) once at insertion time and storing the entry in a slot derived from that number. That single implementation detail explains every rule about keys: a key must be hashable, meaning hash(key) must work and must return the same number every time the object is used as a key. Strings, numbers, tuples of hashables, None, and booleans satisfy this; lists, dicts and sets deliberately do not, because their hash would have to change as their contents change and the entry would become unfindable.
Because lookup compares by equality after matching hashes, keys that are equal are the same key. 1 == 1.0 == True is true in Python and all three hash to 1, so {1: 'a', 1.0: 'b', True: 'c'} is a one-entry dictionary. When a duplicate key is assigned, the value is replaced but the original key object stays in place, so the surviving key prints as 1 rather than True. The same collapsing happens inside a single literal, which is why a literal with a repeated key is legal code rather than an error.
There are four common construction forms and they are not interchangeable. A literal {'a': 1} accepts any hashable key expression. dict(a=1) uses keyword syntax, so it only produces keys that are valid identifier strings. dict(pairs) consumes any iterable of two-item pairs, which makes dict(zip(names, values)) the natural way to join two parallel sequences, and dict.fromkeys(keys, value) builds keys from an iterable while assigning the same value object to all of them.
prices = {"apple": 1.5, "banana": 0.75}
from_pairs = dict([("apple", 1.5), ("banana", 0.75)])
from_kwargs = dict(apple=1.5, banana=0.75)
zipped = dict(zip(["apple", "banana"], [1.5, 0.75]))
print(prices == from_pairs == from_kwargs == zipped)
dupes = {"a": 1, "b": 2, "a": 3}
print(dupes)
collide = {1: "int", 1.0: "float", True: "bool"}
print(collide)
mixed = {"name": "ada", 42: "answer", (2, 3): "point", None: "missing"}
print(list(mixed))
try:
{[1, 2]: "nope"}
except TypeError as e:
print("TypeError:", e)A dictionary key must be hashable and is identified by equality, so equal keys are one key no matter how the dictionary was built.
Worked examples
dict.fromkeys and its shared value
Shows fromkeys building keys from any iterable and handing every key the same value object.
flags = dict.fromkeys(["read", "write", "exec"], False)
print(flags)
buckets = dict.fromkeys("ab", [])
buckets["a"].append(1)
print(buckets)
print(dict.fromkeys("banana"))Example explained
Line 1fromkeys takes an iterable of keys, so a list of strings gives three boolean entries.
Line 2The second argument is evaluated once, so both keys reference the identical list and one append shows up twice.
Line 3Iterating the string "ab" yields characters, making 'a' and 'b' separate keys.
Line 4With no second argument the value defaults to None, and duplicate characters in "banana" collapse to b, a, n in first-seen order.
Tuples as composite keys
Demonstrates why a tuple can index a grid but a tuple containing a list cannot.
grid = {}
grid[(0, 0)] = "start"
grid[(1, 2)] = "goal"
print(grid[(1, 2)])
print(hash((1, 2)) == hash((1, 2)))
try:
grid[(1, [2])] = "bad"
except TypeError as e:
print("TypeError:", e)Example explained
Line 1A brand new (1, 2) tuple finds the stored entry because equal tuples hash the same.
Line 2hash of a tuple is derived from the hashes of its items, which is why the comparison is True.
Line 3(1, [2]) is itself immutable, but hashing it must hash the inner list, so the whole key is rejected.
Important notes
A duplicate key in a literal is not an error and produces no warning, so typos silently discard the earlier value.
Assigning to an existing equal key replaces the value but keeps the original key object, so {1.0: 'a'} then d[1] = 'b' still prints the key as 1.0.
Common mistakes
Using a list as a key, for example counts[[1, 2]] = 3, which raises TypeError: unhashable type: 'list' at the moment of assignment.
Calling dict.fromkeys(keys, []) and then appending to one entry, which mutates the single shared list so every key appears to change.
Writing dict('a'=1) or dict(1='x') expecting quoted or numeric keyword keys, which is a SyntaxError; keyword form only accepts bare identifier names.
Try it yourself
Change, predict, then run
Build the same three-entry dictionary of country codes to names four ways (literal, dict of pairs, dict with keywords, dict(zip(...))) and print whether all four compare equal, then add a (row, col) tuple key to one of them and print its value.
Open the Python workspaceCheck your understanding
What does len({1: 'a', True: 'b', 1.0: 'c'}) evaluate to, and what does the dictionary contain?
- 1, containing {1: 'c'}
- 3, containing {1: 'a', True: 'b', 1.0: 'c'}
- 2, containing {1: 'c', True: 'b'}
- It raises a SyntaxError because the key 1 is repeated
Show answer
True == 1 == 1.0 and all three hash to 1, so each assignment lands in the same slot and only the last value 'c' survives, giving one entry. The 3-entry answer assumes different types mean different keys, but dictionaries identify keys by equality and hash, not by type; the key printed stays 1 because the first key object inserted is kept.