ABCs with bogus __subclasses__ break instance/subclass checks for other ABCs
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 77.2k
- Forks
- 35.9k
- PR merge metrics
- PR metrics pending
Description
Bug report
Bug description:
Consider this evil class:
import abc
class EvilABC(abc.ABC):
__subclasses__ = ...
Now all instance/subclass checks for other ABCs fail:
>>> isinstance(..., abc.ABC)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
isinstance(..., abc.ABC)
~~~~~~~~~~^^^^^^^^^^^^^^
File "<frozen abc>", line 119, in __instancecheck__
File "<frozen abc>", line 123, in __subclasscheck__
File "<frozen abc>", line 123, in __subclasscheck__
TypeError: attribute of type 'ellipsis' is not callable
This happens because the recursive call to __subclasses__ in https://github.com/python/cpython/blob/v3.13.0a5/Lib/_py_abc.py#L141 trusts that __subclasses__ will always be callable and return an iterable (or a list in the C implementation).
You might argue that the EvilABC class is evil and whoever does that is doomed to be punished. However, it can easily be done by a mistake. This class has a broken __subclasses__ without explicitly defining it:
class Crossbreed(abc.ABCMeta, os.PathLike):
...
>>> isinstance("/", os.PathLike)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
isinstance("/", os.PathLike)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "<frozen abc>", line 119, in __instancecheck__
File "<frozen abc>", line 123, in __subclasscheck__
File "<frozen abc>", line 123, in __subclasscheck__
TypeError: unbound method type.__subclasses__() needs an argument
It is very surprising that by defining a class we can break checks for other classes and hence third party code, even from the standard library:
$ python3 -c 'import abc, os, subprocess
> class A(abc.ABCMeta, os.PathLike): pass
> subprocess.run(("echo",))'
Traceback (most recent call last):
File "<string>", line 3, in <module>
File "/usr/lib64/python3.12/subprocess.py", line 548, in run
with Popen(*popenargs, **kwargs) as process:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.12/subprocess.py", line 1026, in __init__
self._execute_child(args, executable, preexec_fn, close_fds,
File "/usr/lib64/python3.12/subprocess.py", line 1802, in _execute_child
elif isinstance(args, os.PathLike):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen abc>", line 119, in __instancecheck__
File "<frozen abc>", line 123, in __subclasscheck__
File "<frozen abc>", line 123, in __subclasscheck__
TypeError: unbound method type.__subclasses__() needs an argument
In my opinion, the __subclasses__ call should be guarded by a try-except and either:
- skip broken subclasses, or
- skip broken subclasses and warn about them, or
- raise a better exception message, saying which subclass is broken
See also https://discuss.python.org/t/abcmeta-change-isinstancecheck-of-additional-parent-class/19908
CPython versions tested on:
3.8, 3.9, 3.10, 3.11, 3.12, 3.13, CPython main branch
Operating systems tested on:
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Lib/_py_abc.py at the recursive subclasses call and reproduce both examples from the issue. Then compare that path with the C implementation mentioned in the report. Done means broken subclasses definitions no longer make unrelated ABC instance or subclass checks fail, with regression coverage for the reported cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100