Skip to content

list.__add__ raises instead of returning NotImplemented #103585

@Gouvernathor

Description

@Gouvernathor

Bug report

list.__add__([], ()) raises a TypeError instead of returning NotImplemented

Your environment

  • CPython versions tested on: "Python 3.11.2 (tags/v3.11.2:878ead1) [MSC v.1934 64 bit (AMD64)] on win32" (hope this helps)
  • Operating system and architecture: Win10 64

I asked a question about this on Discourse, and tbh I only partially understood the answer but it still doesn't seem to square with what the documentation says.
To quote the section about __add__ :

If one of those methods does not support the operation with the supplied arguments, it should return NotImplemented.

The sentence seems clear enough, in my opinion, not to leave much room to interpretation about whether to consider this behavior a bug or not.
This seems to be an edge case failure of an optimization.


If this is not deemed to be an important bug, here is my use-case and why I believe it yields an inconsistent behavior (no need to read if you already think it's a bug).
I'm creating a frozenlist - a tuple that adds and equals with lists, basically - in an environment where there's a subclass of list called RevertableList. Little is necessary to know about it, apart that its __add__ is overridden but calls list.__add__.

Here is a short-ish reproducing code :
python_list = list

class RevertableList(list):
    def __add__(self, other):
        rv = list.__add__(self, other)
        if rv == NotImplemented:
            return rv
        return type(self)(rv)

    def __repr__(self):
        return type(self).__name__ + super().__repr__()

class frozenlist(tuple):
    __slots__ = ()

    def __eq__(self, other):
        if isinstance(other, frozenlist):
            return super().__eq__(other)
        elif isinstance(other, python_list):
            return super().__eq__(tuple(other))
        else:
            return False

    def __getitem__(self, key):
        rv = super().__getitem__(key)
        if isinstance(key, slice):
            return type(self)(rv)
        return rv

    def __add__(self, other):
        if isinstance(other, tuple) and not isinstance(other, frozenlist):
            return NotImplemented
        if isinstance(other, python_list):
            other = tuple(other)
        return type(self)(super().__add__(other))

    def __radd__(self, other):
        print("radd", other)
        if isinstance(other, python_list):
            return other + python_list(self)
        return NotImplemented

    def __mul__(self, other):
        return type(self)(super().__mul__(other))

    def __rmul__(self, other):
        return type(self)(super().__rmul__(other))

    def __repr__(self):
        return type(self).__name__ + super().__repr__()

# assert list.__add__(python_list(), ()) is NotImplemented # fails
assert RevertableList() + python_list() == RevertableList()
assert frozenlist() + python_list() == frozenlist()
assert python_list() + frozenlist() == python_list() # __radd__ gets called, triggers the print
assert python_list() + RevertableList() == python_list()
assert frozenlist() + RevertableList() == frozenlist()
assert RevertableList() + frozenlist() == RevertableList() # fails before __radd__ gets called

As you can see, RevertableList should behave like list in all but repr, direct type checking and maybe performances. However, the order in which methods get called is not the same, for no reason I was able to find in the doc.
NB : I know sometimes the datamodel allows for the method call to be reversed, calling __radd__ on the right object before trying __add__ on the left object, but 1) if it applied here it would save the bug from being triggered 2) RevertableList and frozenlist's only common base class is object, since frozenlist is not a real nor virtual subclass of list.

Metadata

Metadata

Assignees

No one assigned

    Labels

    docsDocumentation in the Doc dirinterpreter-core(Objects, Python, Grammar, and Parser dirs)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions