diff --git a/CHANGES.rst b/CHANGES.rst
index 175f4ae..48b6cb4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,9 +4,21 @@
 4.5.2 (unreleased)
 ------------------
 
-- Fixed ``PersistentList`` to mark itself as changed after calling
+- Fix ``PersistentList`` to mark itself as changed after calling
   ``clear``. See `PR 115 <https://github.com/zopefoundation/persistent/pull/115/>`_.
 
+- Fix ``PersistentMapping.update`` to accept keyword arguments like
+  the native ``UserDict``. Previously, most uses of keyword arguments
+  resulted in ``TypeError``; in the undocumented and extremely
+  unlikely event of a single keyword argument called ``b`` that
+  happens to be a dictionary, the behaviour will change. Also adjust
+  the signatures of ``setdefault`` and ``pop`` to match the native
+  version.
+
+- Fix ``PersistentList.clear``, ``PersistentMapping.clear`` and
+  ``PersistentMapping.popitem`` to no longer mark the object as
+  changed if it was empty.
+
 4.5.1 (2019-11-06)
 ------------------
 
diff --git a/persistent/mapping.py b/persistent/mapping.py
index 554999a..60b0b03 100644
--- a/persistent/mapping.py
+++ b/persistent/mapping.py
@@ -64,34 +64,68 @@ class PersistentMapping(IterableUserDict, persistent.Persistent):
         self._p_changed = 1
 
     def clear(self):
+        """
+        Remove all data from this dictionary.
+
+        .. versionchanged:: 4.5.2
+           If there was nothing to remove, this object is no
+           longer marked as modified.
+        """
+        # Historically this method always marked ourself as changed,
+        # so if there was a _container it was persisted as data. We want
+        # to preserve that, even if we won't make any modifications otherwise.
+        needs_changed = '_container' in self.__dict__ or bool(self)
+        # Python 2 implements this by directly calling self.data.clear(),
+        # but Python 3 does so by repeatedly calling self.popitem()
         self.__super_clear()
+        if needs_changed:
+            self._p_changed = 1
+            assert self._p_changed
+
+    def update(self, *args, **kwargs):
+        """
+        D.update([E, ]**F) -> None.
+
+        .. versionchanged:: 4.5.2
+           Now accepts arbitrary keyword arguments. In the special case
+           of a keyword argument named ``b`` that is a dictionary,
+           the behaviour will change.
+        """
+        self.__super_update(*args, **kwargs)
         self._p_changed = 1
 
-    def update(self, b):
-        self.__super_update(b)
-        self._p_changed = 1
+    def setdefault(self, key, *args, **kwargs): # pylint:disable=arguments-differ
+        # (Python 3 and Python 2 have very different signatures.)
 
-    def setdefault(self, key, failobj=None):
         # We could inline all of UserDict's implementation into the
         # method here, but I'd rather not depend at all on the
         # implementation in UserDict (simple as it is).
-        if not key in self.data:
+        if key not in self.data:
             self._p_changed = 1
-        return self.__super_setdefault(key, failobj)
+        return self.__super_setdefault(key, *args, **kwargs)
 
-    def pop(self, key, *args):
+    def pop(self, key, *args, **kwargs): # pylint:disable=arguments-differ
+        # (Python 3 and Python 2 have very different signatures.)
         self._p_changed = 1
-        return self.__super_pop(key, *args)
+        return self.__super_pop(key, *args, **kwargs)
 
     def popitem(self):
+        """
+        Remove an item.
+
+        .. versionchanged:: 4.5.2
+           No longer marks this object as modified if it was empty
+           and an exception raised.
+        """
+        result = self.__super_popitem()
         self._p_changed = 1
-        return self.__super_popitem()
+        return result
 
     # Old implementations used _container rather than data.
     # Use a descriptor to provide data when we have _container instead
 
     @default
-    def data(self):
+    def data(self): # pylint:disable=method-hidden
         # We don't want to cause a write on read, so we're careful not to
         # do anything that would cause us to become marked as changed, however,
         # if we're modified, then the saved record will have data, not

PersistentMapping.update does not accept keyword arguments
We define the signature as: https://github.com/zopefoundation/persistent/blob/d4f1204d87bec113de5c2457eaa7878ec0f40ab3/persistent/mapping.py#L70

But the actual signature is supposed to be 
```python
# Python 3 collections.UserDict.update
    def update(self, other=(), /, **kwds):
        ''' D.update([E, ]**F) -> None.  Update D from mapping/iterable E and F.
            If E present and has a .keys() method, does:     for k in E: D[k] = E[k]
            If E present and lacks .keys() method, does:     for (k, v) in E: D[k] = v
            In either case, this is followed by: for k, v in F.items(): D[k] = v
        '''
# Python 2 UserDict.UserDict.update
    def update(*args, **kwargs):
       pass
# Python 2 dict.update docstring matches Python 3
```

So the behaviour is not the same:
```pycon
>>> from collections import UserDict
>>> from persistent.mapping import PersistentMapping
>>> UserDict().update(a=1, b=2)
>>> PersistentMapping().update(a=1, b=2)
TypeError: update() got an unexpected keyword argument 'a'
>>> PersistentMapping().update(b=2)
TypeError: 'int' object is not iterable
```

I think the best we can do is probably `(*args, **kwargs)`.
diff --git a/persistent/tests/test_list.py b/persistent/tests/test_list.py
index d7bdbe9..34a6017 100644
--- a/persistent/tests/test_list.py
+++ b/persistent/tests/test_list.py
@@ -16,6 +16,8 @@
 
 import unittest
 
+from persistent.tests.utils import TrivialJar
+
 l0 = []
 l1 = [0]
 l2 = [0, 1]
@@ -30,6 +32,7 @@ class OtherList:
     def __getitem__(self, i):
         return self.__data[i]
 
+
 class TestPList(unittest.TestCase):
 
     def _getTargetClass(self):
@@ -37,10 +40,7 @@ class TestPList(unittest.TestCase):
         return PersistentList
 
     def _makeJar(self):
-        class Jar(object):
-            def register(self, obj):
-                "no-op"
-        return Jar()
+        return TrivialJar()
 
     def _makeOne(self, *args):
         inst = self._getTargetClass()(*args)
diff --git a/persistent/tests/test_mapping.py b/persistent/tests/test_mapping.py
index c252378..d93fadc 100644
--- a/persistent/tests/test_mapping.py
+++ b/persistent/tests/test_mapping.py
@@ -13,6 +13,8 @@
 ##############################################################################
 import unittest
 
+from persistent.tests.utils import TrivialJar
+
 # pylint:disable=blacklisted-name, protected-access
 
 class Test_default(unittest.TestCase):
@@ -53,8 +55,13 @@ class PersistentMappingTests(unittest.TestCase):
         from persistent.mapping import PersistentMapping
         return PersistentMapping
 
-    def _makeOne(self, *args, **kw):
-        return self._getTargetClass()(*args, **kw)
+    def _makeJar(self):
+        return TrivialJar()
+
+    def _makeOne(self, *args, **kwargs):
+        inst = self._getTargetClass()(*args, **kwargs)
+        inst._p_jar = self._makeJar()
+        return inst
 
     def test_volatile_attributes_not_persisted(self):
         # http://www.zope.org/Collectors/Zope/2052
@@ -66,7 +73,6 @@ class PersistentMappingTests(unittest.TestCase):
         self.assertFalse('_v_baz' in state)
 
     def testTheWorld(self):
-        from persistent._compat import PYTHON2
         # Test constructors
         l0 = {}
         l1 = {0:0}
@@ -83,6 +89,7 @@ class PersistentMappingTests(unittest.TestCase):
 
         class OtherMapping(dict):
             def __init__(self, initmapping):
+                dict.__init__(self)
                 self.__data = initmapping
             def items(self):
                 raise AssertionError("Not called")
@@ -221,6 +228,59 @@ class PersistentMappingTests(unittest.TestCase):
         self.assertEqual(pm.__dict__, {'data': {'a': 1}})
         self.assertEqual(pm.__getstate__(), {'data': {'a': 1}})
 
+    def test_update_keywords(self):
+        # Prior to https://github.com/zopefoundation/persistent/issues/126,
+        # PersistentMapping didn't accept keyword arguments to update as
+        # the builtin dict and the UserDict do.
+        # Here we make sure it does. We use some names that have been
+        # seen to be special in signatures as well to make sure that
+        # we don't interpret them incorrectly.
+        pm = self._makeOne()
+        # Our older implementation was ``def update(self, b)``, so ``b``
+        # is potentially a keyword argument in the wild; the behaviour in that
+        # corner case has changed.
+        pm.update(b={'a': 42})
+        self.assertEqual(pm, {'b': {'a': 42}})
+
+        pm = self._makeOne()
+        # Our previous implementation would explode with a TypeError
+        pm.update(b=42)
+        self.assertEqual(pm, {'b': 42})
+
+        pm = self._makeOne()
+        # ``other`` shows up in a Python 3 signature.
+        pm.update(other=42)
+        self.assertEqual(pm, {'other': 42})
+        pm = self._makeOne()
+        pm.update(other={'a': 42})
+        self.assertEqual(pm, {'other': {'a': 42}})
+
+        pm = self._makeOne()
+        pm.update(a=1, b=2)
+        self.assertEqual(pm, {'a': 1, 'b': 2})
+
+    def test_clear_nonempty(self):
+        pm = self._makeOne({'a': 42})
+        self.assertFalse(pm._p_changed)
+        pm.clear()
+        self.assertTrue(pm._p_changed)
+
+    def test_clear_empty(self):
+        pm = self._makeOne()
+        self.assertFalse(pm._p_changed)
+        pm.clear()
+        self.assertFalse(pm._p_changed)
+
+    def test_clear_empty_legacy_container(self):
+        pm = self._makeOne()
+        pm.__dict__['_container'] = pm.__dict__.pop('data')
+        self.assertFalse(pm._p_changed)
+        pm.clear()
+        # Migration happened
+        self.assertIn('data', pm.__dict__)
+        # and we are marked as changed.
+        self.assertTrue(pm._p_changed)
+
 
 class Test_legacy_PersistentDict(unittest.TestCase):
 
@@ -230,7 +290,7 @@ class Test_legacy_PersistentDict(unittest.TestCase):
 
     def test_PD_is_alias_to_PM(self):
         from persistent.mapping import PersistentMapping
-        self.assertTrue(self._getTargetClass() is PersistentMapping)
+        self.assertIs(self._getTargetClass(), PersistentMapping)
 
 
 def test_suite():
diff --git a/persistent/tests/utils.py b/persistent/tests/utils.py
index 41eb2ba..320c546 100644
--- a/persistent/tests/utils.py
+++ b/persistent/tests/utils.py
@@ -1,4 +1,13 @@
 
+class TrivialJar(object):
+    """
+    Jar that only supports registering objects so ``_p_changed``
+    can be tested.
+    """
+
+    def register(self, ob):
+        """Does nothing"""
+
 class ResettingJar(object):
     """Testing stub for _p_jar attribute.
     """

pip install -e .[test]
parse_log_pytest
pytest
zope.testrunner pytest
apt-get update apt-get install -y gcc
3.8
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
cffi==1.17.1
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
manuel==1.13.0
packaging @ file:///croot/packaging_1720101850331/work
-e git+https://github.com/zopefoundation/persistent.git@f91227e6a01a7cfcc29beba0d3ea384deecb3bc3#egg=persistent
pluggy @ file:///tmp/build/80754af9/pluggy_1648042571233/work
pycparser==2.22
pytest @ file:///croot/pytest_1717793244625/work
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
zope.exceptions==5.2
zope.interface==7.2
zope.testrunner==6.6.1

name: persistent
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=py38h06a4308_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.1=py38h06a4308_0
  - pip=24.2=py38h06a4308_0
  - pluggy=1.0.0=py38h06a4308_1
  - pytest=7.4.4=py38h06a4308_0
  - python=3.8.20=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.1.0=py38h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py38h06a4308_0
  - wheel=0.44.0=py38h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - cffi==1.17.1
      - manuel==1.13.0
      - pycparser==2.22
      - zope-exceptions==5.2
      - zope-interface==7.2
      - zope-testrunner==6.6.1
prefix: /opt/conda/envs/persistent

persistent/tests/test_mapping.py::PersistentMappingTests::test_clear_empty persistent/tests/test_mapping.py::PersistentMappingTests::test_update_keywords
persistent/tests/test_list.py::TestPList::testTheWorld persistent/tests/test_list.py::TestPList::test_append persistent/tests/test_list.py::TestPList::test_clear_empty persistent/tests/test_list.py::TestPList::test_clear_nonempty persistent/tests/test_list.py::TestPList::test_delslice_empty persistent/tests/test_list.py::TestPList::test_delslice_nonempty persistent/tests/test_list.py::TestPList::test_extend persistent/tests/test_list.py::TestPList::test_iadd persistent/tests/test_list.py::TestPList::test_imul persistent/tests/test_list.py::TestPList::test_insert persistent/tests/test_list.py::TestPList::test_remove persistent/tests/test_list.py::TestPList::test_reverse persistent/tests/test_list.py::TestPList::test_setslice persistent/tests/test_list.py::TestPList::test_volatile_attributes_not_persisted persistent/tests/test_list.py::test_suite persistent/tests/test_mapping.py::Test_default::test___get___from_class persistent/tests/test_mapping.py::Test_default::test___get___from_instance persistent/tests/test_mapping.py::PersistentMappingTests::testTheWorld persistent/tests/test_mapping.py::PersistentMappingTests::test___repr___converts_legacy_container_attr persistent/tests/test_mapping.py::PersistentMappingTests::test_clear_empty_legacy_container persistent/tests/test_mapping.py::PersistentMappingTests::test_clear_nonempty persistent/tests/test_mapping.py::PersistentMappingTests::test_volatile_attributes_not_persisted persistent/tests/test_mapping.py::Test_legacy_PersistentDict::test_PD_is_alias_to_PM persistent/tests/test_mapping.py::test_suite
persistent/mapping.py CHANGES.rst
persistent/mapping.py CHANGES.rst