diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index be25a94e7e94f6..24741ab177ed95 100644 --- a/Doc/library/abc.rst +++ b/Doc/library/abc.rst @@ -223,6 +223,16 @@ The :mod:`!abc` module also provides the following decorator: return any(getattr(f, '__isabstractmethod__', False) for f in (self._fget, self._fset, self._fdel)) + :class:`ABCMeta` only inspects the object that a subclass finally binds to + an abstract name: if that object does not report itself as abstract, the + name counts as implemented. Nothing checks that a descriptor replacing an + abstract one still provides the same components. Overriding the read-write + ``my_abstract_property`` above with a read-only :deco:`property`, for + example, does not prevent the subclass from being instantiated, and the + missing setter is reported only as an :exc:`AttributeError` when the + attribute is assigned to. A :term:`static type checker` can flag such an + override; the abstract base class machinery does not. + .. note:: Unlike Java abstract methods, these abstract @@ -318,6 +328,12 @@ The :mod:`!abc` module also supports the following legacy decorators: def x(self, val): ... + This borrows the remaining components from ``C.x``, but it is not the only + way to override an abstract property. As described for + :deco:`abstractmethod` above, binding the name to a new, non-abstract + :deco:`property` also makes the attribute concrete, even if every component + of the original was abstract and the replacement provides fewer of them. + The :mod:`!abc` module also provides the following functions: diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 814d7fff2f4135..87c946f2c452b0 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -288,6 +288,35 @@ class E(D): def foo(self, val): pass self.assertFalse(E.foo.__isabstractmethod__) + def test_concrete_override_drops_abstract_components(self): + # Only the object a subclass finally binds to an abstract name is + # inspected; nothing checks that it still provides the components + # of the descriptor it replaces. See gh-83888. + class C(metaclass=abc_ABCMeta): + @property + @abc.abstractmethod + def foo(self): ... + @foo.setter + @abc.abstractmethod + def foo(self, val): ... + self.assertEqual(C.__abstractmethods__, {"foo"}) + # A read-only property makes the read-write abstract property + # concrete, silently dropping the setter. + class D(C): + @property + def foo(self): return 3 + self.assertEqual(D.__abstractmethods__, set()) + d = D() + self.assertEqual(d.foo, 3) + with self.assertRaises(AttributeError): + d.foo = 4 + # The same holds for an abstract name that is not a descriptor at + # all: any non-abstract object counts as an implementation. + class E(C): + foo = 3 + self.assertEqual(E.__abstractmethods__, set()) + self.assertEqual(E().foo, 3) + def test_metaclass_abc(self): # Metaclasses can be ABCs, too. class A(metaclass=abc_ABCMeta): diff --git a/Misc/NEWS.d/next/Documentation/2026-09-25-13-10-00.gh-issue-83888.Rt4nQv.rst b/Misc/NEWS.d/next/Documentation/2026-09-25-13-10-00.gh-issue-83888.Rt4nQv.rst new file mode 100644 index 00000000000000..5c45f4ba5ea521 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2026-09-25-13-10-00.gh-issue-83888.Rt4nQv.rst @@ -0,0 +1,3 @@ +Document that :class:`~abc.ABCMeta` only inspects the object a subclass binds +to an abstract name, so overriding an abstract read-write property with a +read-only one leaves the class instantiable.