Search code examples
pythonlistdictionaryoperator-overloading

Why is plus-equals valid for list and dictionary?


Adding a dictionary to a list using the __iadd__ notation seems to add the keys of the dictionary as elements in the list. Why? For example

a = []
b = {'hello': 'world'}
a += b
print(a)  # -> ['hello']

The documentation for plus-equals on collections doesn't imply to me that this should happen:

For instance, to execute the statement x += y, where x is an instance of a class that has an __iadd__() method, x.__iadd__(y) is called. If x is an instance of a class that does not define a __iadd__() method, x.__add__(y) and y.__radd__(x) are considered, as with the evaluation of x + y

But, logically, both a + b and b + a raise a TypeError. Furthermore, b += a raises a TypeError too. I don't see any special implementation in the source that would explain things, but I'm not 100% sure where to look.

The closest question on SO I found is this one, asking about += on dictionaries, but that's just asking about a data structure with itself. This one had a promising title about list self-addition, but it claims "__add__" is being applied under the hood, which shouldn't be defined between lists and dictionaries.

My best guess is that the __iadd__ is invoking extend, which is defined here, and then it tries to iterate over the dictionary, which in turn yields its keys. But this seems... weird? And I don't see any intuition of that coming from the docs.


Solution

  • My best guess is that the __iadd__ is invoking extend, which is defined here, and then it tries to iterate over the dictionary, which in turn yields its keys. But this seems... weird? And I don't see any intuition of that coming from the docs.

    This is the correct answer for why this happens. I've found the relevant docs that say this-

    In the docs you can see that in fact __iadd__ is equivalent to .extend(), and here it says:

    list.extend(iterable): Extend the list by appending all the items from the iterable.

    In the part about dicts it says:

    Performing list(d) on a dictionary returns a list of all the keys used in the dictionary

    So to summarize, a_list += a_dict is equivalet to a_list.extend(iter(a_dict)), which is equivalent to a_list.extend(a_dict.keys()), which will extend the list with the list of keys in the dictionary.

    We can maybe discuss on why this is the way things are, but I don't think we will find a clear-cut answer. I think += is a very useful shorthand for .extend, and also that a dictionary should be iterable (personally I'd prefer it returning .items(), but oh well)


    Edit: You seem to be interested in the actual implementation of CPython, so here are some code pointers:

    dict iterator returning keys:

    static PyObject *
    dict_iter(PyDictObject *dict)
    {
        return dictiter_new(dict, &PyDictIterKey_Type);
    }
    

    list.extend(iterable) calling iter() on its argument:

    static PyObject *
    list_extend(PyListObject *self, PyObject *iterable)
    {
        ...
        it = PyObject_GetIter(iterable);
        ...
    }
    

    += being equivalent to list.extend():

    static PyObject *
    list_inplace_concat(PyListObject *self, PyObject *other)
    {
        ...
        result = list_extend(self, other);
        ...
    }
    

    and then this method seems to be referenced above inside a PySequenceMethods struct, which seems to be an abstraction of sequences that defines common actions such as concatenating in-place, and concatenating normally (which is defined as list_concat in the same file and you can see is not the same).