20

I have face this weird behavior I can not find explications about.

MWE:

l = [1]
l += {'a': 2}
l
[1, 'a']
l + {'B': 3}
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: can only concatenate list (not "dict") to list

Basically, when I += python does not raise an error and append the key to the list while when I only compute the + I get the expected TypeError.

Note: this is Python 3.6.10

ClementWalter
  • 4,814
  • 1
  • 32
  • 54
  • 1
    Check this [https://stackoverflow.com/questions/15376509/when-is-i-x-different-from-i-i-x-in-python](https://stackoverflow.com/questions/15376509/when-is-i-x-different-from-i-i-x-in-python) – Mayur Fartade Apr 27 '20 at 10:52
  • 2
    See https://bugs.python.org/issue9314 - "inconsistent result when concatenating list with iterators". Also note `+=` and so on are called "Augmented assignments" from the PEP introducing them, [PEP 203](https://www.python.org/dev/peps/pep-0203/). – alkasm Apr 27 '20 at 10:52
  • That is something strange – Dhruv Agarwal Apr 27 '20 at 10:54
  • 6
    I think the bug link by @alkasm has a statment that explains it well. `When a is mutable, a += b updates it in-place, so there is no ambiguity: the type of a cannot change. When you do a + b, there is no reason to treat a as more deserving than b when selecting the type of the result.` – Chris Doyle Apr 27 '20 at 10:58
  • @ChrisDoyle that is the exact reasoning. It is not "a bug", it's a mechanism that keeps the interpreter from guessing. – DeepSpace Apr 27 '20 at 11:00
  • Yeah i just added it as a comment here to make it clear why the behavior happens as there is quite a lot of info in the link. But yes its not a bug its expected behavior, the comment just helps explain why it works like it does – Chris Doyle Apr 27 '20 at 11:02
  • @ChrisDoyle that's the case. see my detailed answer below. cheers – seralouk May 09 '20 at 18:38

2 Answers2

17

l += ... is actually calling object.__iadd__(self, other) and modifies the object in-place when l is mutable

The reason (as @DeepSpace explains in his comment) is that when you do l += {'a': 2} the operation updates l in place only and only if l is mutable. On the other hand, the operation l + {'a': 2} is not done in place resulting into list + dictionary -> TypeError.


(see here)


l = [1]
l = l.__iadd__({'a': 2})
l
#[1, 'a']

is not the same as + that calls object.__add__(self, other)

l + {'B': 3}
TypeError: can only concatenate list (not "dict") to list
seralouk
  • 30,938
  • 9
  • 118
  • 133
  • 7
    This should also explain the reasoning. `l += ...` updates `l` in place, so the type of the result is clear (= the type of `l`). `l + ...` is ambiguous because it is not in place, so the type of the resulting new object is not clear (should it be the type of `l` or should it be the type of `...`?) – DeepSpace Apr 27 '20 at 10:58
  • 1
    of course! I added this to my answer. The `l += ...` operates **in-place** – seralouk Apr 27 '20 at 11:20
  • @seralouk, do you mind sharing which tool you used to found out which function was invoked ? Inspect, pdb etc ? – surya May 07 '20 at 05:20
  • the documentation: https://docs.python.org/3/reference/datamodel.html#object.__iadd__ and some programming knowledge e.g. dictionary are mutables. – seralouk May 07 '20 at 07:45
  • @DeepSpace the reasoning makes sense, but `__iadd__` is not guaranteed to return the same object. See `number += 2`. Also, from docs: These methods should attempt to do the operation in-place (modifying self) and return the result (which could be, but does not have to be, self) – Capi Etheriel May 07 '20 at 16:03
  • @CapiEtheriel It's not about returning the same object, but about returning the same **type**. Also, `int` is not a good example as it is a special case, `a = 1 ; a += 2.0`, now `a` is float. But, `int` is immutable to begin with, so "in-place" has no meaning. – DeepSpace May 07 '20 at 16:39
1

So as the authors say this is not a bug. When you Do a += b it is like b come to a's house and changing it the way that a like it to be. what the Authors say is when you do a + b it cannot be decided which one's style will get prioritized. and no one knows where will the result of a + b will go until you execute it. So you can't decide whose style it would be. if it is a style it would be [1, 'a']'s, and if it is b style it would be an error. and therefore it cannot be decided who will get the priority. So I don't personally agree with that statement. because when you take the call stack a is in a higher place than b. when there is a expression like a + b you first call a.__add__(self, other) if a.__add__ is NotImplemented (in this case it is implemented). then you call a.__radd__(self, other). which means call other.__add__ in this case b.__add__. I am telling this based on the place of the call stack and the python community may have more important reasons to do this.

Hyperx837
  • 773
  • 5
  • 13