bryevdv: @philippjfr I have updated the test I think it should be better now
philippjfr: Looks good and works for me.
diff --git a/bokeh/core/property/bases.py b/bokeh/core/property/bases.py
index c5d7c92c9..4aace587a 100644
--- a/bokeh/core/property/bases.py
+++ b/bokeh/core/property/bases.py
@@ -178,7 +178,19 @@ class Property(PropertyDescriptorFactory):
         if isinstance(new, np.ndarray) or isinstance(old, np.ndarray):
             return np.array_equal(new, old)
 
-        return new == old
+        # this handles the special but common case where there is a dict with numpy
+        # arrays as values (e.g. the .data property of a ColumnDataSource)
+        if isinstance(new, dict) and isinstance(old, dict):
+            if set(new.keys()) != set(old.keys()):
+                return False
+            return all(self.matches(new[k], old[k]) for k in new)
+
+        try:
+            return new == old
+
+        # if the comparison fails for some reason, just punt and return no-match
+        except ValueError:
+            return False
 
     def from_json(self, json, models=None):
         ''' Convert from JSON-compatible values into a value for this property.

Modifying datasources broken
In bokeh master and presumably any rc/dev releases cut in the last few days, modifying a CDS is broken completely. This is because https://github.com/bokeh/bokeh/pull/6374 introduced a change in the way bokeh properties check whether an old and new value has changed. The approach there guards against checks on arrays, but since the ``.data`` property on a CDS is not itself an array it ends up doing a simple equality between the dictionary of arrays, which raises this error:

>ValueError�: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

This occurs for any change to a ``ColumnDataSource.data``, which means that any server or notebook examples that modify a CDS in python are currently broken.
diff --git a/bokeh/core/property/tests/test_bases.py b/bokeh/core/property/tests/test_bases.py
index 14a1234bc..d7a0309ab 100644
--- a/bokeh/core/property/tests/test_bases.py
+++ b/bokeh/core/property/tests/test_bases.py
@@ -45,6 +45,14 @@ def test_property_assert_msg_funcs():
         p.prepare_value(hp, "foo", 10)
         assert str(e) == "bad True name, 10"
 
+def test_property_matches_basic_types(capsys):
+    p = pb.Property()
+    for x in [1, 1.2, "a", np.arange(4), None, False, True, {}, []]:
+        assert p.matches(x, x) is True
+        assert p.matches(x, "junk") is False
+    out, err = capsys.readouterr()
+    assert err == ""
+
 def test_property_matches_compatible_arrays(capsys):
     p = pb.Property()
     a = np.arange(5)
@@ -65,3 +73,35 @@ def test_property_matches_incompatible_arrays(capsys):
     out, err = capsys.readouterr()
     # no way to suppress FutureWarning in this case
     # assert err == ""
+
+def test_property_matches_dicts_with_array_values(capsys):
+    p = pb.Property()
+    d1 = dict(foo=np.arange(10))
+    d2 = dict(foo=np.arange(10))
+
+    assert p.matches(d1, d1) is True
+    assert p.matches(d1, d2) is True
+
+    # XXX not sure if this is preferable to have match, or not
+    assert p.matches(d1, dict(foo=list(range(10)))) is True
+
+    assert p.matches(d1, dict(foo=np.arange(11))) is False
+    assert p.matches(d1, dict(bar=np.arange(10))) is False
+    assert p.matches(d1, dict(bar=10)) is False
+    out, err = capsys.readouterr()
+    assert err == ""
+
+def test_property_matches_non_dict_containers_with_array_false(capsys):
+    p = pb.Property()
+    d1 = [np.arange(10)]
+    d2 = [np.arange(10)]
+    assert p.matches(d1, d1) is True  # because object identity
+    assert p.matches(d1, d2) is False
+
+    t1 = (np.arange(10),)
+    t2 = (np.arange(10),)
+    assert p.matches(t1, t1) is True  # because object identity
+    assert p.matches(t1, t2) is False
+
+    out, err = capsys.readouterr()
+    assert err == ""

pip install bokeh
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
bokeh==3.4.3
contourpy==1.3.0
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.1.6
MarkupSafe==3.0.2
numpy==2.0.2
packaging @ file:///croot/packaging_1734472117206/work
pandas==2.2.3
pillow==11.1.0
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.2
six==1.17.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
tornado==6.4.2
tzdata==2025.2
xyzservices==2025.1.0

name: bokeh
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - bokeh==3.4.3
      - contourpy==1.3.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - numpy==2.0.2
      - pandas==2.2.3
      - pillow==11.1.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.2
      - six==1.17.0
      - tornado==6.4.2
      - tzdata==2025.2
      - xyzservices==2025.1.0
prefix: /opt/conda/envs/bokeh

bokeh/core/property/tests/test_bases.py::test_property_matches_dicts_with_array_values bokeh/core/property/tests/test_bases.py::test_property_matches_non_dict_containers_with_array_false
bokeh/core/property/tests/test_bases.py::test_property_assert_bools bokeh/core/property/tests/test_bases.py::test_property_assert_functions bokeh/core/property/tests/test_bases.py::test_property_assert_msg_funcs bokeh/core/property/tests/test_bases.py::test_property_matches_basic_types bokeh/core/property/tests/test_bases.py::test_property_matches_compatible_arrays bokeh/core/property/tests/test_bases.py::test_property_matches_incompatible_arrays
bokeh/core/property/bases.py
bokeh/core/property/bases.py