mrkeyoor.com_
Thu 06 Aug 07:39 UTC
PyPIUtilsupdated 06 Aug 2026

deepdiff

DeepDiff compares two arbitrary Python objects and tells you what changed, recursively. Give it two nested dicts, two lists of objects, or two class instances and it returns a report keyed by change type: values_changed, dictionary_item_added, iterable_item_removed, type_changes, and so on, with each entry addressed by a path string like root['users'][0]['email']. Around that core it ships three companions: DeepSearch for finding a value anywhere inside a structure, DeepHash for content-based hashing of unhashable objects, and Delta for turning a diff into a portable patch you can add to another object later. The options list is where the real work happens, letting you ignore list ordering, ignore int-versus-float differences, tolerate floating point noise, or skip paths like timestamps that always change.

Verdict

The most capable deep-comparison library in Python, and the only one that handles arbitrary objects with this many knobs for ordering, tolerance, and exclusions. Reach for it when the structures are genuinely complex; for plain JSON or a single test assertion, jsonpatch or dirty-equals will leave you with less to explain.

API stability3/5The DeepDiff(t1, t2) call has held for years, but the result keys and path strings are effectively the API and have shifted across majors, the Delta serialization format is version-coupled, and 9.x raised the Python floor to 3.10 and swapped in new dependencies.
Docs4/5zepworks.com/deepdiff documents every module and nearly every option with runnable examples and keeps versioned copies online; the gap is that the options interact in ways the reference does not explain, so figuring out why ignore_order plus report_repetition behaves as it does still means experimenting.
Maintenance4/5Pushed August 2026 and 9.1.0 shipped real work (parallel diffing, a security fix in Delta path handling, type-hint corrections), but 99 issues are open (104 counting PRs), the project has effectively one maintainer, and it recently moved from the author's personal account into a company org whose commercial product is now the first thing in the README.
Ecosystem4/5Around 19 million weekly downloads, largely as a transitive dependency of test and data tooling, and it ships its own CLI, DeepSearch, DeepHash, and Delta modules; there is little third-party plugin ecosystem around it, so what you get is what the package includes.

Use it if

  • You assert on large nested structures in tests and want a readable report of what differs instead of a pytest dump of two 400-line dicts side by side
  • You compare API responses, config files, or database snapshots where list ordering is not meaningful and ignore_order=True saves you from writing sort keys for every nested collection
  • You need to compare data that json.dumps cannot handle: custom class instances, sets, Decimals, datetimes, named tuples, and objects with __slots__
  • You want a patch you can move between processes: Delta serializes a diff to bytes, and applying it to the original object reproduces the new one, with bidirectional=True letting you undo
  • You need content-based hashing of nested unhashable objects, which DeepHash gives you without writing a canonical serializer yourself
Skip it if

Setup reality

pip install deepdiff is quick but it is no longer a pure-Python install: as of 9.x it depends on cachebox, which ships a compiled extension, plus orderly-set, and it needs Python 3.10 or newer. Prebuilt wheels cover the common platforms; an unusual architecture or a locked-down build environment means compiling a Rust extension you did not ask for. Two extras matter: pip install "deepdiff[cli]" for the deep command line tool (it pulls click and PyYAML), and pip install "deepdiff[optimize]" for orjson-backed serialization. The real setup cost is learning the path syntax, because exclude_paths, include_paths, and extract all take strings like root['a'][0]['b'] and a typo silently matches nothing rather than erroring. The wildcard support added in 9.1 uses an unquoted star inside the brackets, so root[*]['ts'] works and root['*']['ts'] quietly does nothing. Anything involving ignore_order also wants attention to cache_size and cutoff_distance_for_pairs before you point it at real data.

Patterns

Diff two nested structuresbasic-diff

from deepdiff import DeepDiff

t1 = {"a": 1, "b": [1, 2, 3], "c": {"d": "x"}}
t2 = {"a": 2, "b": [1, 3, 2], "c": {"d": "X"}, "e": 9}

DeepDiff(t1, t2)
# {'dictionary_item_added': ["root['e']"],
#  'values_changed': {"root['a']": {'new_value': 2, 'old_value': 1},
#                     "root['b'][1]": {'new_value': 3, 'old_value': 2},
#                     "root['b'][2]": {'new_value': 2, 'old_value': 3},
#                     "root['c']['d']": {'new_value': 'X', 'old_value': 'x'}}}

An empty result is falsy, so `if DeepDiff(t1, t2):` is the idiomatic equality check. Note that reordering b produced two values_changed entries: by default positions are compared index by index, which is almost never what you want for unordered data.

Compare collections where order does not matterignore-order

from deepdiff import DeepDiff

DeepDiff({"b": [1, 2, 3]}, {"b": [1, 3, 2]}, ignore_order=True)
# {}

DeepDiff([1, 1, 2], [1, 2], ignore_order=True, report_repetition=True)
# {'repetition_change': {'root[0]': {'old_repeat': 2, 'new_repeat': 1,
#                                    'old_indexes': [0, 1], 'new_indexes': [0], 'value': 1}}}

This is the expensive flag. DeepDiff has to pair up elements across both sides, which is roughly quadratic and gets much worse with nested dicts inside the lists. Tune cache_size and cutoff_distance_for_pairs before running it on anything large. Without report_repetition, duplicate elements are treated as one.

Skip fields that always changeexclude-paths

from deepdiff import DeepDiff

t1 = {"a": {"ts": 1, "v": 2}, "b": {"ts": 1, "v": 2}}
t2 = {"a": {"ts": 9, "v": 2}, "b": {"ts": 9, "v": 3}}

DeepDiff(t1, t2, exclude_paths=["root['a']['ts']", "root['b']['ts']"])
# {'values_changed': {"root['b']['v']": {'new_value': 3, 'old_value': 2}}}

DeepDiff(t1, t2, exclude_paths=["root[*]['ts']"])   # wildcard, added in 9.1
# {'values_changed': {"root['b']['v']": {'new_value': 3, 'old_value': 2}}}

The wildcard is an unquoted star inside the brackets. Writing root['*']['ts'] with quotes matches a literal key named * and excludes nothing, with no error to tell you. Verify your exclusions actually shrink the diff before trusting them in CI.

Limit the diff to a subtree or a patterninclude-and-regex-paths

from deepdiff import DeepDiff

DeepDiff(t1, t2, include_paths=["root['b']"])
# only differences under b

DeepDiff(t1, t2, exclude_regex_paths=[r"\['ts'\]"])
# every key named ts, at any depth

Regex paths are matched against the generated path string, so you escape the brackets, not the keys. include_paths is the safer choice when you only care about a known subtree, because a new field appearing elsewhere will not start failing your comparison.

Tolerate floating point noisenumeric-tolerance

from deepdiff import DeepDiff

DeepDiff({"p": 1.0001}, {"p": 1.0002}, significant_digits=3)   # {}
DeepDiff({"p": 1.0}, {"p": 1.0001}, math_epsilon=0.001)        # {}

significant_digits rounds both sides to that many decimal places before comparing; math_epsilon uses an absolute tolerance via math.isclose. They are different tools: rounding trips on values straddling a boundary, epsilon does not. Do not pass both.

Stop int-versus-float and str-versus-int noiseignore-type-changes

from deepdiff import DeepDiff

DeepDiff({"n": 1}, {"n": 1.0})
# {'type_changes': {"root['n']": {'old_type': int, 'new_type': float,
#                                 'old_value': 1, 'new_value': 1.0}}}

DeepDiff({"n": 1}, {"n": 1.0}, ignore_numeric_type_changes=True)   # {}
DeepDiff({"n": 1}, {"n": "1"}, ignore_type_in_groups=[(int, str)])
# {'values_changed': {"root['n']": {'new_value': '1', 'old_value': 1}}}

ignore_numeric_type_changes handles the common JSON round-trip case where 1 comes back as 1.0. ignore_type_in_groups only stops the type_changes report; the values are still compared, so 1 and '1' still differ. Add ignore_string_type_changes for bytes versus str.

Turn a diff into JSON or Englishrender-the-diff

from deepdiff import DeepDiff

diff = DeepDiff(t1, t2)

diff.to_json()
# '{"dictionary_item_added": ["root[\'e\']"], "values_changed": {...}}'

print(diff.pretty())
# Item root['e'] added to dictionary.
# Value of root['a'] changed from 1 to 2.

diff.affected_root_keys   # SetOrdered(['a', 'b', 'c', 'e'])

to_json needs the values to be JSON-serializable and takes a default_mapping for the ones that are not. affected_paths and affected_root_keys are the quick way to answer 'which fields changed' without walking the nested report yourself.

Store a diff and apply it laterdelta-patch

from deepdiff import DeepDiff, Delta

delta = Delta(DeepDiff({"a": 1}, {"a": 2}))
{"a": 1} + delta          # {'a': 2}

reversible = Delta(DeepDiff({"a": 1}, {"a": 2}), bidirectional=True)
{"a": 2} - reversible     # {'a': 1}

blob = delta.dumps()      # bytes, for storage or transport
Delta(blob) is not None

Subtraction only works when the Delta was created with bidirectional=True, which makes it larger. The serialized form is Python-specific and version-coupled, so do not treat it as an interchange format between services on different DeepDiff versions; use jsonpatch for that.

Hash nested and unhashable objects by contentcontent-hash

from deepdiff import DeepHash

obj = {"a": [1, 2], "b": {"c": 3}}
hashes = DeepHash(obj)
hashes[obj]
# 'e6a3...'  content hash, stable across runs for equal structures

DeepHash returns a mapping from every object it walked to that object's hash, not a single value, so you index it with the object you care about. It is what powers ignore_order internally. Lookups are by identity for unhashable children, so keep the original references around.

Find a value anywhere in a structuredeep-search

from deepdiff import grep

{"a": {"b": "find me"}} | grep("find")
# {'matched_values': ["root['a']['b']"]}

from deepdiff import DeepSearch
DeepSearch({"a": {"b": "find me"}}, "find", verbose_level=2)
# {'matched_values': {"root['a']['b']": 'find me'}}

The pipe operator with grep is the same thing as DeepSearch with nicer syntax. Matching is substring-based on strings and exact on other types; pass use_regexp=True for patterns. verbose_level=2 returns the matched values, not just the paths.

Read a value using a diff path stringextract-by-path

from deepdiff import extract

extract({"a": [{"b": 5}]}, "root['a'][0]['b']")   # 5

This closes the loop: take a path out of a diff report and pull the current value from either side without hand-parsing the string. It raises on a path that does not exist, which is a useful check that your exclude_paths strings are spelled correctly.

Define your own comparison rule for a typecustom-operator

from deepdiff import DeepDiff
from deepdiff.operator import BaseOperator

class ApproxFloat(BaseOperator):
    def give_up_diffing(self, level, diff_instance):
        return abs(level.t1 - level.t2) < 1.0   # True means 'treat as equal'

DeepDiff({"p": 1.0}, {"p": 1.5}, custom_operators=[ApproxFloat(types=[float])])
# {}

Returning True from give_up_diffing means DeepDiff stops descending and reports nothing for that node; return False to fall through to normal comparison. Report a difference yourself with diff_instance.custom_report_result. Custom operators disable the parallel diffing added in 9.1, so a large comparison gets slower.

Alternatives

PackageRegistryPick it when
jsonpatchPyPIBoth sides are JSON and you need a standard RFC 6902 patch that services in other languages can apply.
dictdifferPyPIYou want a small, dependency-light diff of nested dicts and lists with patch and revert, and none of the tolerance or ordering options.
dirty-equalsPyPIThe goal is a readable test assertion with approximate matchers, not a diff report you programmatically inspect.
jsondiffPyPIYou want the diff itself expressed as a JSON-shaped structure you can store and ship, with several syntax styles to choose from.