The three questions this function asks are the three basic set operations, wearing different words.
before = {'host': 'a', 'port': 80, 'debug': False}
after = {'host': 'a', 'port': 443, 'tls': True}
before.keys() - after.keys() # {'debug'} -> removed
after.keys() - before.keys() # {'tls'} -> added
before.keys() & after.keys() # {'host', 'port'} -> candidates
# Of the keys in both, only those whose values differ have changed
[k for k in before.keys() & after.keys() if before[k] != after[k]] # ['port']
- dict.keys() is already a set
You do not need set(before) — .keys() returns a view that supports -, & and | directly, without building a copy. Difference is 'in this one and not the other', intersection is 'in both'. Naming the operation rather than writing the loop means the reader sees the intent instead of reconstructing it from a conditional.
- Set operations lose the order, so put it back deliberately
A set has no order, so iterating one of these results directly gives an arbitrary arrangement that can differ between runs. Iterating the original dictionary and testing membership against the set keeps the author’s order and is just as fast — the membership test is O(1). Use the set to decide, use the dict to order.
- The changed entry holds both values because one is not enough
Reporting only the new value tells the reader what the state is, not what happened — and 'port is 443' is far less useful in an audit log than 'port went from 80 to 443'. A tuple of (before, after) is the smallest thing that carries the whole change, and it is what makes the output reversible.
Three separate questions, three separate dictionary comprehensions. Work out which keys belong in each answer before you worry about the values.
before.keys() - after.keys() is what was removed; the other way round is what was added; & is the keys worth comparing. Build each result by walking the original dictionary and testing membership, so the order comes from the data rather than from the set.
gone = before.keys() - after.keys(), new = after.keys() - before.keys(). Then removed = {k: v for k, v in before.items() if k in gone}, added = {k: v for k, v in after.items() if k in new}, and changed = {k: (before[k], after[k]) for k in before if k in after and before[k] != after[k]}.
def diff_dicts(before, after): gone = before.keys() ____ after.keys() new = after.keys() ____ before.keys() return { 'added': {k: v for k, v in ____.items() if k in new}, 'removed': {k: v for k, v in ____.items() if k in gone}, 'changed': { key: (before[key], after[key]) for key in before if key in after and before[key] ____ after[key] }, }
def diff_dicts(before, after):
gone = before.keys() - after.keys()
new = after.keys() - before.keys()
return {
"added": {key: value for key, value in after.items() if key in new},
"removed": {key: value for key, value in before.items() if key in gone},
"changed": {
key: (before[key], after[key])
for key in before
if key in after and before[key] != after[key]
},
}
The set difference answers 'which keys', and then the comprehension walks the original dictionary to build the result — so membership is a constant-time test and the output order is the author’s. Iterating the set directly would be shorter and would make the output order an implementation detail of Python’s hashing, which is not something to put in an audit log.
`for key in before if key in after` is the intersection, expressed in the place where the order already exists. Computing before.keys() & after.keys() and then iterating it would be the same work plus a shuffle. Reach for the set operation when it makes the intent clearer, not on principle.
Returning the empty dictionaries rather than omitting them is what lets callers write `for key in diff['added']` with no guard, and it means the shape of the answer does not depend on the data. A result whose keys come and go forces every consumer to defend itself, and one of them will forget.
For nested structures this is the wrong tool — it will tell you 'limits changed' and leave you to find out how. DeepDiff handles arbitrary nesting, lists and type changes, and produces paths like root['limits']['cpu']; for JSON specifically, RFC 6902 patches (jsonpatch) give a diff that can be applied as well as read. And if you are diffing two versions of a file rather than two dictionaries, difflib is already in the standard library. Write this when the data is flat, the output is for a human, and you want to control exactly what the three categories mean.
- Make it recurse into nested dictionaries, and design the output shape before you write any code — that is the hard half.
- Write apply_diff(before, diff) that reproduces after, then check the round trip on every case here.
- Add an ignore argument for keys that always differ — timestamps, request ids — and notice how much noise it removes from a real config diff.