deepdiff review
DeepDiff 9.1.0 walks two Python object graphs and groups each mismatch by kind and path: changed values, added keys, type changes, moved iterable items, or repetition counts. It understands dictionaries, sequences, sets, named tuples, dates, decimals, and user objects rather than limiting input to JSON. The distribution also contains DeepHash for content-based hashes, DeepSearch for nested lookup, Extract for following DeepDiff paths, and Delta for replaying changes. The current release adds eligible multiprocessing work, wildcard path filters, a new cache, typing corrections, and an early block on dunder traversal in Delta paths.
DeepDiff 9.1.0 installed in 0.3 seconds and used 2 MB in our sandbox, with a working 0.70-second import and 0 audit findings. Use it for explainable Python object differences; use JSON Patch for cross-language changes and a focused assertion helper for tiny tests.
We installed it
| Install | ✓ · 0.3s | 3 packages on disk · 2 MB |
| Import | ✓ | import deepdiff in 0.70s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does deepdiff install cleanly?
Yes. In a fresh container with an empty cache, pip install deepdiff finished in 0.3s, leaving 3 packages and 2 MB on disk. pip-audit reported no known vulnerabilities.
What does deepdiff need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import deepdiff succeeded in 0.70s, and the package ships py.typed for type checkers.
deepdiff or jsonpatch: which should you use?
jsonpatch: Choose it when another language must consume or apply standard RFC 6902 JSON operations. DeepDiff 9.1.0 installed in 0.3 seconds and used 2 MB in our sandbox, with a working 0.70-second import and 0 audit findings.
When should you not use deepdiff?
Changes cross a service boundary as JSON; RFC 6902 operations from jsonpatch are language-neutral, while DeepDiff Delta is tied to Python behavior
Use it if
- A failed test must show the precise path and old and new values inside a nested Python object
- Sequence order carries no meaning and matching elements by their content is worth extra CPU time
- Comparison rules need numeric tolerance, ignored paths, custom operators, or selected type equivalence
- One Python codebase needs recursive comparisons together with content hashing, nested search, or replayable deltas
- Changes cross a service boundary as JSON; RFC 6902 operations from jsonpatch are language-neutral, while DeepDiff Delta is tied to Python behavior
- Very large lists are usually compared without order; element pairing, distance calculations, and repetition tracking can dominate runtime
- The deliverable is a stable report for people; category dictionaries and root-style paths still need a presentation layer
- A test checks only two or three approximate fields; pytest.approx or dirty-equals keeps that intent next to the assertion
- Any deployed interpreter is Python 3.9 or earlier; DeepDiff 9 dropped 3.9 and declares Python 3.10 as its floor
Setup reality
We installed DeepDiff 9.1.0 without a cache in a fresh Python 3.12 Bookworm sandbox. Installation took 0.3 seconds and left 3 packages totaling 2 MB. The lab metadata counted 33 direct dependencies, and pip-audit reported 0 known vulnerabilities. This is a pure-Python package requiring Python 3.10 or newer, with an MIT license and a py.typed marker. Importing deepdiff succeeded in 0.70 seconds.
The base package asks for no account, token, or project file. Install deepdiff[cli] for its shell command and deepdiff[optimize] for optional orjson serialization. YAML, TOML, CSV, and Pydantic handling each depend on separate packages. Pin the extras the application actually calls; installing deepdiff alone does not promise every loader shown in the documentation.
Version 9.1 path filters use strings such as root['orders'][0]['created_at']. Glob support accepts an unquoted star segment in include_paths and exclude_paths. A syntactically valid path may match nothing, so keep a fixture containing one known ignored change. Regex filters match the rendered path string, which makes quoting and brackets part of the rule.
Setting ignore_order=True replaces positional comparison with content matching. Large repeated collections may need cache and cutoff tuning. Version 9.1 can parallelize distance work and subtree comparisons, but custom operators, object callbacks, and ignore_order_func trigger a serial fallback. Treat serialized Delta bytes as package-specific Python data, and never load a Delta supplied by an untrusted party.
Patterns
Locate changes inside nested objects basic-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'}}}The result is falsy when no category contains a difference. Lists use their positions unless you select unordered matching.
Match list members by content ignore-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}}}ignore_order performs element pairing instead of index comparison. report_repetition preserves a mismatch in duplicate counts.
Omit volatile object paths exclude-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}}}Version 9.1 recognizes an unquoted star as a glob segment. Keep a fixture that proves the rule hides the intended timestamp.
Limit the walk to selected branches include-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 depthinclude_paths narrows traversal to named branches. exclude_regex_paths runs against DeepDiff's rendered root path.
Treat nearby numbers as equal numeric-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 compares rounded representations, whereas math_epsilon sets an absolute gap. They encode different domain rules.
Choose which numeric types are equivalent ignore-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}}}A permitted type group suppresses the type mismatch only. Different values can still appear under values_changed.
Serialize or summarize a result render-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 a conversion for values outside JSON's data model. affected_root_keys is a smaller signal for routing downstream work.
Replay and reverse a recorded change delta-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 NoneSubtraction works only for a bidirectional Delta. Serialized Delta bytes can drive object traversal, so reject data from untrusted sources.
Hash an object by its contents hash-nested-content
from deepdiff import DeepHash
record = {"tags": {"blue", "green"}, "meta": {"active": True}}
hashes = DeepHash(record)
content_hash = hashes[record]DeepHash handles nested Python values that built-in hash rejects. Changing a nested member produces a different digest.
Find text in keys and values search-nested-values
from deepdiff import DeepSearch
config = {"database": {"host": "db.internal"}, "owner": "ops"}
result = DeepSearch(config, "internal", case_sensitive=False)
print(result)DeepSearch reports matched paths in categorized sets. It searches the Python object graph rather than querying an external index.
Read a value from a reported path extract-by-path
from deepdiff import extract
order = {"lines": [{"sku": "A1"}, {"sku": "B2"}]}
value = extract(order, "root['lines'][1]['sku']")
assert value == "B2"extract follows DeepDiff's root path syntax. Do not pass an attacker-controlled path into an object that exposes sensitive attributes.
Compare record lists by identity match-records-by-id
from deepdiff import DeepDiff
before = [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Lin"}]
after = [{"id": 2, "name": "Linus"}, {"id": 1, "name": "Ada"}]
diff = DeepDiff(before, after, group_by="id")group_by turns each list into records indexed by the selected field. Duplicate or missing identifiers make that comparison unsuitable.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| jsonpatch | PyPI | Choose it when another language must consume or apply standard RFC 6902 JSON operations. |
| dictdiffer | PyPI | Choose it for a smaller dictionary and sequence differ that can patch and revert its tuples. |
| jsondiff | PyPI | Choose it when inputs stay JSON-shaped and a compact diff syntax is sufficient. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

