Dictionary Diff
The diff utilities provide two functions for working with dictionary changes: generate_diff() creates a minimal diff between two dictionaries, and apply_diff() merges a diff back into a dictionary. These functions are used extensively throughout the Doover platform for tag management, aggregate updates, and efficient state synchronisation.
Generating a Diff
generate_diff() compares two dictionaries and returns only the keys that differ:
from pydoover.utils import generate_diff
old = {"temperature": 22.5, "humidity": 60, "mode": "auto"}
new = {"temperature": 23.1, "humidity": 60, "mode": "manual", "fan_speed": 3}
diff = generate_diff(old, new)
# Result: {"temperature": 23.1, "mode": "manual", "fan_speed": 3}
The diff contains only changed or added keys. Keys with identical values in both dictionaries are omitted.
Handling Deletions
When a key exists in the old dictionary but not in the new one, it appears in the diff with a value of None:
old = {"temperature": 22.5, "humidity": 60, "mode": "auto"}
new = {"temperature": 22.5, "humidity": 60}
diff = generate_diff(old, new)
# Result: {"mode": None}
To disable deletion tracking, set do_delete=False. Removed keys will simply be absent from the diff:
diff = generate_diff(old, new, do_delete=False)
# Result: {}
Nested Dictionaries
Diffs are generated recursively for nested structures:
old = {
"sensors": {
"temp": {"value": 22.5, "unit": "C"},
"pressure": {"value": 101.3, "unit": "kPa"},
}
}
new = {
"sensors": {
"temp": {"value": 23.1, "unit": "C"},
"pressure": {"value": 101.3, "unit": "kPa"},
}
}
diff = generate_diff(old, new)
# Result: {"sensors": {"temp": {"value": 23.1}}}
Only the changed leaf value appears in the diff. Unchanged branches are omitted entirely.
Applying a Diff
apply_diff() merges a diff into an existing dictionary:
from pydoover.utils import apply_diff
data = {"temperature": 22.5, "humidity": 60, "mode": "auto"}
diff = {"temperature": 23.1, "mode": "manual", "fan_speed": 3}
result = apply_diff(data, diff)
# Result: {"temperature": 23.1, "humidity": 60, "mode": "manual", "fan_speed": 3}
Deletion Behaviour
By default, keys with None values in the diff are removed from the target:
data = {"temperature": 22.5, "humidity": 60, "mode": "auto"}
diff = {"mode": None}
result = apply_diff(data, diff)
# Result: {"temperature": 22.5, "humidity": 60}
To keep None values instead of deleting keys, set do_delete=False:
result = apply_diff(data, diff, do_delete=False)
# Result: {"temperature": 22.5, "humidity": 60, "mode": None}
Immutable vs In-Place Operation
By default, apply_diff() creates a deep copy of the input data, leaving the original unchanged:
data = {"temperature": 22.5}
diff = {"temperature": 23.1}
result = apply_diff(data, diff)
# data is still {"temperature": 22.5}
# result is {"temperature": 23.1}
For performance-sensitive code, set clone=False to modify the dictionary in place:
result = apply_diff(data, diff, clone=False)
# data and result are the same object: {"temperature": 23.1}
Nested Application
Diffs are applied recursively. Nested dictionaries are merged rather than replaced:
data = {
"sensors": {
"temp": {"value": 22.5, "unit": "C"},
"pressure": {"value": 101.3, "unit": "kPa"},
}
}
diff = {"sensors": {"temp": {"value": 23.1}}}
result = apply_diff(data, diff)
# Result:
# {
# "sensors": {
# "temp": {"value": 23.1, "unit": "C"},
# "pressure": {"value": 101.3, "unit": "kPa"},
# }
# }
The pressure object is preserved and the temp.unit field is untouched -- only temp.value is updated.
Non-Dict Inputs
When either input to apply_diff() is not a dictionary, the diff value is returned directly:
result = apply_diff("old_string", "new_string")
# Result: "new_string"
The same rule applies to generate_diff() -- if the new value is not a dictionary, it is returned as the entire diff.
Helper: maybe_load_json
The maybe_load_json() function attempts to parse a string as JSON. If parsing fails, it returns the original value unchanged:
from pydoover.utils import maybe_load_json
data = maybe_load_json('{"temperature": 22.5}')
# Result: {"temperature": 22.5}
data = maybe_load_json("not json")
# Result: "not json"
Function Reference
| Function | Returns | Description |
|---|---|---|
generate_diff(old, new, do_delete=True) | dict | Minimal diff between two dictionaries. |
apply_diff(data, diff, do_delete=True, clone=True) | dict | Merge a diff into a dictionary. |
maybe_load_json(data) | Any | Parse JSON string or return original value. |
Related Pages
- Utilities Overview -- all available utility modules