Skip to main content

A lightweight library to compare JSON, dicts, dataclasses, and Pydantic models with tolerance and ignore rules.

Project description

dictlens

Deep structural comparison for Python dicts with per-field numeric tolerance and JSONPath-like targeting.

Overview

dictlens is a lightweight Python library for comparing two nested dict structures (or any dict-like objects) with fine-grained tolerance control.

It supports:

  • ✅ Global absolute (abs_tol) and relative (rel_tol) numeric tolerances
  • ✅ Per-field tolerance overrides via JSONPath-like expressions
  • ✅ Ignoring volatile or irrelevant fields
  • ✅ Detailed debug logs that explain why two structures differ

It’s ideal for comparing API payloads, serialized models, ML metrics, or configurations where small numeric drifts are expected.

Installation

pip install dictlens

Quick Examples

from dictlens import compare_dicts

a = {"sensor": {"temp": 21.5, "humidity": 48.0}}
b = {"sensor": {"temp": 21.7, "humidity": 48.5}}

# Default tolerances
res = compare_dicts(a, b, abs_tol=0.05, rel_tol=0.01, show_debug=True)
print(res)  # False
### Output (debug)

[NUMERIC COMPARE] $.sensor.temp: 21.5 vs 21.7 | diff=0.200000 | abs_tol=0.05 | rel_tol=0.01 | threshold=0.217000
[MATCH NUMERIC] $.sensor.temp: within tolerance
[NUMERIC COMPARE] $.sensor.humidity: 48.0 vs 48.5 | diff=0.500000 | abs_tol=0.05 | rel_tol=0.01 | threshold=0.485000
[FAIL NUMERIC] $.sensor.humidity  diff=0.500000 > threshold=0.485000
[FAIL IN DICT] $.sensor.humidity
[FAIL IN DICT] $..sensor

Ignore Fields

from dictlens import compare_dicts


def test_ignore_path_root_field():
    a = {"id": 1, "timestamp": "now"}
    b = {"id": 1, "timestamp": "later"}

    # Ignore only the root timestamp field
    ignore_fields = ["$.timestamp"]

    result = compare_dicts(a, b, ignore_fields=ignore_fields)
    print(result)  # True


def test_ignore_fields_complex():
    """
    Ignore multiple paths with different patterns:
      - Exact path:            $.user.profile.updated_at
      - Array wildcard:        $.devices[*].debug
      - Recursive descent key: $..trace
    """
    a = {
        "user": {
            "id": 7,
            "profile": {"updated_at": "2025-10-14T10:00:00Z", "age": 30}
        },
        "devices": [
            {"id": "d1", "debug": "alpha", "temp": 20.0},
            {"id": "d2", "debug": "beta", "temp": 20.1}
        ],
        "sessions": [
            {"events": [{"meta": {"trace": "abc"}, "value": 10.0}]},
            {"events": [{"meta": {"trace": "def"}, "value": 10.5}]}
        ]
    }

    b = {
        "user": {
            "id": 7,
            "profile": {"updated_at": "2025-10-15T10:00:05Z", "age": 30}
        },
        "devices": [
            {"id": "d1", "debug": "changed", "temp": 20.05},
            {"id": "d2", "debug": "changed", "temp": 20.18}
        ],
        "sessions": [
            {"events": [{"meta": {"trace": "xyz"}, "value": 10.01}]},
            {"events": [{"meta": {"trace": "uvw"}, "value": 10.52}]}
        ]
    }

    # Ignore updated_at (exact), all device.debug (wildcard), any 'trace' anywhere (recursive)
    ignore_fields = [
        "$.user.profile.updated_at",
        "$.devices[*].debug",
        "$..trace",
    ]

    # Small global tolerance to allow minor sensor/value drift
    result = compare_dicts(
        a, b,
        ignore_fields=ignore_fields,
        abs_tol=0.05,
        rel_tol=0.02
    )
    print(result)  # True

Per-field Tolerances

You can override tolerances for specific paths using JSONPath-like expressions.

from dictlens import compare_dicts

a = {"a": 1.0, "b": 2.0}
b = {"a": 1.5, "b": 2.5}
abs_tol_fields = {"$.b": 1.0}
result = compare_dicts(a, b,abs_tol=0.5, abs_tol_fields=abs_tol_fields) 
print(result)  # True

array specific index tolerance

from dictlens import compare_dicts

a = {"sensors": [{"temp": 20.0}, {"temp": 21.0}]}
b = {"sensors": [{"temp": 20.05}, {"temp": 21.5}]}
abs_tol_fields = {"$.sensors[0].temp": 0.1, "$.sensors[1].temp": 1.0} 
result = compare_dicts(a, b, abs_tol_fields=abs_tol_fields)
print(result) # True

array wildcard tolerance

from dictlens import compare_dicts

a = {"sensors": [{"temp": 20.0}, {"temp": 21.0}]}
b = {"sensors": [{"temp": 20.2}, {"temp": 21.1}]}
abs_tol_fields = {"$.sensors[*].temp": 0.5}
result = compare_dicts(a, b, abs_tol_fields=abs_tol_fields)
print(result) # True

property wildcard tolerance

from dictlens import compare_dicts

a = {"network": {"n1": {"v": 10}, "n2": {"v": 10}}}
b = {"network": {"n1": {"v": 10.5}, "n2": {"v": 9.8}}}
abs_tol_fields = {"$.network.*.v": 1.0}
result = compare_dicts(a, b, abs_tol_fields=abs_tol_fields)
print(result) # True

recursive wildcard tolerance

from dictlens import compare_dicts

a  = {"meta": {"deep": {"very": {"x": 100}}}}
b = {"meta": {"deep": {"very": {"x": 101}}}}
abs_tol_fields = {"$..x": 2.0}
result = compare_dicts(a, b, abs_tol_fields=abs_tol_fields)
print(result)

combined global and field tolerances

from dictlens import compare_dicts

# Original reading (e.g., baseline snapshot)
a = {
    "station": {
        "id": "ST-42",
        "location": "Paris",
        "version": 1.0
    },
    "metrics": {
        "temperature": 21.5,
        "humidity": 48.0,
        "pressure": 1013.2,
        "wind_speed": 5.4
    },
    "status": {
        "battery_level": 96.0,
        "signal_strength": -72
    },
    "timestamp": "2025-10-14T10:00:00Z"
}

# New reading (e.g., after transmission)
b = {
    "station": {
        "id": "ST-42",
        "location": "Paris",
        "version": 1.03  # version drift allowed (custom abs_tol)
    },
    "metrics": {
        "temperature": 21.6,  # tiny drift (global rel_tol ok)
        "humidity": 49.3,  # bigger drift (custom abs_tol ok)
        "pressure": 1013.5,  # tiny drift (global ok)
        "wind_speed": 5.6  # small drift (global ok)
    },
    "status": {
        "battery_level": 94.8,  # within abs_tol
        "signal_strength": -69  # within rel_tol (5%)
    },
    "timestamp": "2025-10-14T10:00:02Z"  # ignored
}

abs_tol_fields = {
    "$.metrics.humidity": 2.0,  # humidity sensors are noisy
    "$.station.version": 0.1  # small version drift allowed
}

rel_tol_fields = {
    "$.status.signal_strength": 0.05,
    "$.metrics.wind_speed": 0.05,
    "$.status.battery_level": 0.02  # allow ±2% battery drift
}

ignore_fields = ["$.meta.id"]

result = compare_dicts(
    a,
    b,
    abs_tol=0.05,
    rel_tol=0.01,
    abs_tol_fields=abs_tol_fields,
    rel_tol_fields=rel_tol_fields,
    ignore_fields=ignore_fields,
    show_debug=True
)

print(result)  # True

Supported Path Patterns

dictlens implements a simplified subset of JSONPath syntax:

Pattern Description
$.a.b Exact field path
$.items[0].price Specific array index
$.items[*].price Any array element
$.data.*.value Any property name
$..x Recursive descent for key x

🔍 Supported and Future JSONPath Features

dictlens currently supports a focused subset of JSONPath syntax designed for simplicity and performance in numeric comparisons.

These patterns cover the vast majority of practical comparison use cases.

At the moment, advanced JSONPath features such as filters ([?()]), unions ([0,1,2]), slices ([0:2]), or expressions are not yet supported. Future versions of dictlens may expand support for these features once performance and readability trade-offs are fully evaluated.

Full API

compare_dicts(
    left: dict,
    right: dict,
    *,
    ignore_fields: list[str] = None,
    abs_tol: float = 0.0,
    rel_tol: float = 0.0,
    abs_tol_fields: dict[str, float] = None,
    rel_tol_fields: dict[str, float] = None,
    epsilon: float = 1e-12,
    show_debug: bool = False,
) -> bool

Arguments

Parameter D escription
left, right The two structures to compare.
ignore_fields Keys to ignore during comparison.
abs_tol Global absolute tolerance for numeric drift.
rel_tol Global relative tolerance.
abs_tol_fields Dict of per-field absolute tolerances.
rel_tol_fields Dict of per-field relative tolerances.
epsilon Small float to absorb FP rounding errors.
show_debug If True, prints detailed comparison logs.

Tips

  • Both dicts must have the same keys — missing keys fail the comparison.
  • Lists are compared in order.
  • For dataclasses or Pydantic models, call .dict() first: compare_dicts(model_a.dict(), model_b.dict())
  • Numeric strings ("1.0") are not treated as numbers. Only real numeric types (int/float) are compared numerically.

Use Cases

  • Snapshot testing of API responses or data models.
  • Machine-learning drift and metric comparison.
  • CI/CD pipelines to verify payload consistency.
  • Configuration or schema diffing.

License

Apache License 2.0 — © 2025 Mohamed Tahri Contributions welcome 🤝

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dictlens-0.1.2.tar.gz (13.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dictlens-0.1.2-py3-none-any.whl (12.0 kB view details)

Uploaded Python 3

File details

Details for the file dictlens-0.1.2.tar.gz.

File metadata

  • Download URL: dictlens-0.1.2.tar.gz
  • Upload date:
  • Size: 13.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for dictlens-0.1.2.tar.gz
Algorithm Hash digest
SHA256 38a0650357ed8c9b6133c205832bc36c2bc22d8cf8b0e2527533c6df1463b18d
MD5 52e19caed5aa9145e595437f70a58a5b
BLAKE2b-256 28d8db4becb2203db210b307e639bf0bba5eb4704cda0fa14b73cd1069aafb1d

See more details on using hashes here.

File details

Details for the file dictlens-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: dictlens-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 12.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for dictlens-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 0478c77402e7481950d6a5fee3b3a2b20b6a05b16e5f17239ff65066ec44ee23
MD5 0b500e6ded426d2f16be316895d4f6d8
BLAKE2b-256 ccdab4a49153c7927cc6a1f7f3d3f647e0ae8b26e4ed8a86749a91abb1f8b38b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page