Skip to content

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

FunctionReturnsDescription
generate_diff(old, new, do_delete=True)dictMinimal diff between two dictionaries.
apply_diff(data, diff, do_delete=True, clone=True)dictMerge a diff into a dictionary.
maybe_load_json(data)AnyParse JSON string or return original value.

Related Pages