How do you deduplicate a list of unhashable items such as dicts or lists in Python?
answer
- The obvious one-liners raise TypeError here
- Do not mutate the items to fix it
- Deduplicate on a stand-in, keep the original
- Sorted pairs, or a sorted serialization
- Last resort is a quadratic equality scan
basics
~20 sMap each item to a hashable stand-in key and deduplicate on that: a tuple for a list, tuple(sorted(d.items())) or a sorted JSON string for a dict. Keep a seen set of those keys and append the original on a miss.
solid answer
~40 sBoth `set()` and `dict.fromkeys` hash their input, so a list of `dict` or `list` instances raises `TypeError`. The fix is a canonical key: something hashable that is equal for exactly the items you consider duplicates. For a flat list, `tuple(item)`. For a flat dict whose values are hashable, `frozenset(d.items())` ignores key order, or `tuple(sorted(d.items()))` if you also want a deterministic representation. For nested structures, `json.dumps(d, sort_keys=True)` flattens everything to a string at the cost of type fidelity, since it cannot distinguish a tuple from a list. Then run the ordinary seen-set loop over those keys while appending the original objects. When no canonical key exists, fall back to an O(n squared) equality scan and only for small inputs.
code
python · 18 linesimport json
rows = [
{"b": 2, "a": 1},
{"a": 1, "b": 2},
{"a": [1, 2]},
{"a": [1, 2]},
]
seen = set()
unique = []
for row in rows:
key = json.dumps(row, sort_keys=True)
if key not in seen:
seen.add(key)
unique.append(row)
print(unique)go deeper
Know that sets and dict keys require hashable values, so a list of dicts raises TypeError, and that the way out is deduplicating on a hashable stand-in such as a tuple of the dict's sorted pairs.
Explain the canonicalize-then-deduplicate pattern and compare the candidate keys: tuple for lists, frozenset or sorted tuple of items for flat dicts, sorted JSON for nested data, and what each one costs or loses.
Treat the canonical function as the definition of duplicate-ness and defend it: which fields participate, whether key order matters, what happens with nested values, and how you test that it merges and separates the right pairs.
Push the problem upstream. Where records must be deduplicated repeatedly, a stable identity, an immutable value type with a real hash, or an explicit content fingerprint on ingest beats every downstream canonicalizer written from guesswork.
## Why the one-liners fail `set(items)` and `dict.fromkeys(items)` both hash every element, so both fail the moment an element is a `dict`, a `list` or a `set`. On CPython 3.14 the error is explicit about what was being attempted: ```pycon >>> set([{"a": 1}]) TypeError: cannot use 'dict' as a set element (unhashable type: 'dict') >>> dict.fromkeys([{"a": 1}]) TypeError: cannot use 'dict' as a dict key (unhashable type: 'dict') ``` Older releases printed only the `unhashable type: 'dict'` half; 3.14 names the container and the role, which is a small but real debugging improvement when the failing expression is a nested comprehension. ## The pattern: canonicalize, then deduplicate The fix is not to make the items hashable, it is to derive a hashable **stand-in** and deduplicate on that while keeping the original objects: ```python seen = set() unique = [] for item in items: key = canonical(item) if key not in seen: seen.add(key) unique.append(item) ``` Everything interesting is in `canonical`, and it is a decision about meaning, not a formality: the key must be equal for exactly the pairs of items you consider duplicates, and different otherwise. ## Choosing a canonical form **A flat list becomes a tuple.** `tuple([1, 2])` is hashable and compares element-wise, so it is the exact hashable mirror of the list. **A flat dict with hashable values has two options.** `frozenset(d.items())` is hashable and, because a frozenset is unordered, treats `{"a": 1, "b": 2}` and `{"b": 2, "a": 1}` as the same key, which is usually what you mean for a mapping. `tuple(sorted(d.items()))` gives the same insensitivity to key order plus a deterministic, printable, comparable key, at the cost of requiring the keys to be mutually orderable. Both break if any value is itself a list or dict. **Nested structures need recursion or serialization.** A small recursive canonicalizer turns lists into tuples and dicts into sorted tuples of pairs, all the way down, and preserves types. The blunter option is `json.dumps(obj, sort_keys=True)`, one line and robust against arbitrary nesting, with three real caveats: it only handles JSON-representable data, it erases the tuple/list distinction so `(1, 2)` and `[1, 2]` become the same key, and it is markedly slower per item than hashing a tuple. For deduplicating parsed JSON payloads, which is where this problem usually arrives from, those caveats are harmless and the one-liner wins. **Sometimes the canonical form is a projection, not a faithful encoding.** If two records are duplicates when their id and timestamp match, regardless of every other field, then the key is just that pair, and no encoding of the whole object is needed. Reaching for a full serialization when a two-field tuple would do costs both CPU and clarity. ## Getting sameness wrong is the real bug The canonical function silently defines your notion of equality, and its failure mode is quiet. A key built from `tuple(d.items())` without sorting makes two dicts with the same pairs in a different insertion order count as distinct, so duplicates survive. A key built from `json.dumps` without `sort_keys=True` has the same defect. Conversely, a key that drops a field that actually distinguishes records merges rows that should have stayed separate, and nothing raises. Test the canonicalizer directly with a pair you know should collapse and a pair you know should not; it is a pure function and trivially testable. ## When there is no canonical key For objects with a meaningful `__eq__` but no hash, and no derivable key, the honest fallback is the quadratic scan: ```python unique = [] for item in items: if not any(item == kept for kept in unique): unique.append(item) ``` It preserves first-seen order and works with equality alone, but it costs O(n squared) comparisons and each comparison may itself be deep. Reserve it for tens of items. If you own the class, the better move is to define `__hash__` alongside `__eq__` on an immutable value type, or make it a `frozen=True` dataclass so both are generated and the plain `dict.fromkeys` idiom works again. ## Interview framing The visible skill is knowing that a `TypeError` about hashability means "derive a key", not "give up on sets". The deeper signal is treating the canonical function as the specification of duplicate-ness, naming its failure modes, and choosing the cheapest key that is still faithful.
- What is the difference between frozenset(d.items()) and tuple(sorted(d.items())) as a canonical key for a dict?Both ignore key insertion order and both require hashable values. The frozenset needs no ordering between keys, so it works with mixed key types, but it has no stable printable form. The sorted tuple is deterministic, comparable and easy to log, but it raises TypeError when the keys are not mutually orderable.
- What does json.dumps lose when used as a canonical key?Type fidelity and generality. It only encodes JSON-representable data, so sets, datetimes and custom objects need a default hook or fail outright, and it collapses the tuple/list distinction, so (1, 2) and [1, 2] produce the same key. It is also far slower per item than hashing a tuple. Without sort_keys=True it is not even canonical.
- When is the O(n squared) equality scan the right answer?When no hashable key faithfully captures sameness and the input is small, typically tens of items. It needs only __eq__, so it works with objects that deliberately have no hash. Past a few hundred items the quadratic comparison count, multiplied by the cost of a deep equality check, makes deriving a key worth the effort instead.
saying these in an interview costs you the question
- Says dicts and lists simply cannot be deduplicated
- Uses str(d) as a key without sorting the keys
- Forgets sort_keys=True when serializing to a key
- Stores the key instead of the original object
- Applies the quadratic scan to very large inputs
- Assumes frozenset(d.items()) works with list values