Hash maps and sets are the single most-used structure in coding interviews, but the four languages disagree sharply on identity vs. equality, iteration order, and which conveniences you get for free. This section is about wielding each one correctly and fast, not about how hashing works internally.
Language Verdict: Pros, Cons & Recommendation
Counter gives free most_common(n), multiset arithmetic, and zero-KeyError counting out of the boxdefaultdict(factory) auto-vivifies missing keys — the cleanest adjacency-list/grouping idiom of the three- Set algebra operators (
&, |, -, ^) are the most readable of the three languages @dataclass(frozen=True) makes a custom hashable key a one-line declaration
- No built-in sorted-dict/sorted-set type —
sorted(d.items()) re-sorts from scratch on every call - Mutating a dict's size mid-iteration raises
RuntimeError immediately, so you must remember to iterate over a copy (list(d)) - No way to pre-size or tune a
dict's load factor for a known element count
TreeMap/TreeSet give a real O(log n) sorted map with range queries (floorKey, ceilingKey) — no equivalent in Python or JScomputeIfAbsent/merge/getOrDefault cover every default-value idiom directly on the Map interface, no extra importsrecord auto-generates a correct equals()/hashCode() pair for custom keys- Initial capacity and load factor are exposed constructor args, letting you pre-size to avoid resize-driven rehashing
- No
Counter equivalent — counting and top-N-by-frequency require manual merge() calls plus a hand-written comparator sort - Set operations (
retainAll/addAll/removeAll) mutate the receiver in place — a missing defensive copy silently destroys the original set - Fail-fast iterators throw
ConcurrentModificationException on any structural change except via Iterator.remove()
map[K]V is a language primitive — no import, no boxing of int keys, comma-ok and delete built in- Structs of comparable fields are valid keys with no
hashCode/equals override - Zero-value reads (
m[k] returns 0/"" if missing) make counting m[k]++ a one-liner - A set is just
map[T]struct{} — tiny, idiomatic, no separate type to remember
- Range order is deliberately randomized — never rely on iteration order (the interview gotcha vs Python 3.7+
dict / JS Map) - No
TreeMap / sorted-map in the stdlib; no defaultdict/Counter — grouping is if _, ok := m[k]; !ok { m[k] = ... } - Nil map panics on write; you must
make (or a composite literal) before the first insert
Map/Set always preserve insertion order, with none of the plain-object integer-key reordering quirkSet.prototype.union/intersection/difference are now standardized (Baseline 2024), removing the need to hand-roll them??= gives a lightweight auto-vivification idiom for plain-object default values
- No
TreeMap equivalent at all — sorted-key access means re-sorting entries on every pass or hand-rolling a balanced structure - No
Counter/defaultdict — counting and grouping are fully manual read-then-write patterns - Mutating a
Map during iteration doesn't throw — added keys are silently visited later and deleted keys silently skipped - Plain objects reorder integer-like string keys ahead of insertion order — a trap if you reach for
{} instead of Map
Recommendation: Python remains the default for maps — Counter/defaultdict and insertion-ordered dicts are unmatched for interview speed. Go's maps are the next-best primitive: unboxed keys, comma-ok, no boxing, but **range order is randomized** (do not port a Python solution that iterates a dict and expects insertion order). Use Java's TreeMap/TreeSet when you actually need sorted keys. In JS, prefer Map over plain objects.
Coding Mechanics, Side by Side
All four languages hash on some notion of equality — but they disagree wildly on what 'equal' means for a custom object, and getting it wrong silently corrupts lookups instead of throwing. Go at least fails at compile time if the key type isn't comparable.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
seen = set()
seen.add(Point(1, 2))
print(Point(1, 2) in seen) # True
@dataclass(frozen=True) auto-generates __hash__ and __eq__ from the declared fields, so two instances with equal field values hash and compare equal. A plain (mutable) @dataclass generates __eq__ but sets __hash__ to None, making it unhashable — you must opt into frozen=True (or write __hash__/__eq__ by hand) to use it as a dict/set key. Plain tuples work too since they're already hashable by value.
d = {}
d["b"] = 1
d["a"] = 2
d["c"] = 3
print(list(d)) # ['b', 'a', 'c']
Insertion-order preservation for dict is a language guarantee as of Python 3.7 (it was a CPython implementation detail in 3.6). set gives NO ordering guarantee at all — iteration order depends on hash values and insertion history and should never be relied on.
This is a JS-specific spec behavior with no real analog in Python, Java, or Go, included here because it's a genuine debugging trap when someone reaches for a plain {} instead of a Map.
d = {"10": "a", "2": "b", "x": "c"}
print(list(d)) # ['10', '2', 'x'] - stays in insertion order
Python dict never reorders keys based on their content — a string key that happens to look like an integer is treated exactly like any other string. There's nothing to watch out for here.
counts = {}
counts["a"] = counts.get("a", 0) + 1
from collections import defaultdict
graph = defaultdict(list)
graph["a"].append("b") # no KeyError, auto-creates []
d = {}
d.setdefault("a", []).append(1)
.get(k, default) is the read-only default; .setdefault(k, default) both reads and inserts the default if missing (returns the existing or newly-inserted value); collections.defaultdict(factory) moves the default-creation logic to construction time so every missing-key access auto-vivifies via factory() — the most common interview idiom for building adjacency lists or grouping maps.
# CPython dict: grows once ~2/3 full; small dicts start with 8 slots.
# No public API to pre-size or tune the load factor.
d = dict() # can't hint an expected size
CPython's dict growth policy and resize threshold are implementation details, not part of the language spec, and there's no way to pre-size a dict/set in pure Python. Building one from an existing iterable (set(items)) can help the allocator but isn't a real capacity hint.
d = {"a": 1, "b": 2}
for k in d:
d["c"] = 3 # RuntimeError: dictionary changed size during iteration
# Safe pattern: iterate over a snapshot
for k in list(d):
d[k + "!"] = d[k]
Adding or removing keys during iteration raises RuntimeError: dictionary changed size during iteration immediately — Python fails fast and loudly. Merely mutating an existing key's VALUE (not adding/removing keys) is safe. The fix is always to iterate over a copy (list(d), list(d.items())).
a = {1, 2, 3}
b = {2, 3, 4}
print(a & b) # {2, 3} intersection
print(a | b) # {1, 2, 3, 4} union
print(a - b) # {1} difference
print(a ^ b) # {1, 4} symmetric difference
All four operators return NEW sets and never mutate a or b. In-place variants exist too (a &= b, a |= b, etc.) if you want to mutate. This is the most ergonomic of the three languages for set math.
from collections import Counter
counts = Counter("mississippi")
print(counts["s"]) # 3
print(counts.most_common(2)) # [('i', 4), ('s', 4)]
Counter is a dict subclass purpose-built for counting: missing keys return 0 instead of raising KeyError (no defaultdict needed), it supports multiset arithmetic (c1 + c2, c1 - c2), and .most_common(n) gives sorted-by-frequency output for free — no manual sort step.
d = {"banana": 3, "apple": 5, "cherry": 1}
for k, v in sorted(d.items()):
print(k, v) # sorted by key, recomputed each time, O(n log n)
# For repeated sorted access, a third-party lib fills the gap:
# from sortedcontainers import SortedDict
Python has no built-in sorted-dict type. The idiomatic approach is sorted(d.items()) on demand — fine for a one-off pass, wasteful if you need repeated sorted access, in which case the well-known third-party sortedcontainers.SortedDict (O(log n) inserts, always-sorted iteration) is worth knowing exists, though you generally can't pip install it mid-interview.