__add__ / __radd__ / __iadd__

def __add__(self, other)
Returns: Any · Added in v3.0 · Updated March 20, 2026 · Dunder Methods
operators dunder-methods operator-overloading

Overview

Three dunder methods control addition behaviour on custom classes:

  • __add__ — called for self + other
  • __radd__ — called as a fallback when the left operand does not support the operation
  • __iadd__ — called for self += other

All three are available in Python 3.0 with no import required.

When Python evaluates a + b, it first tries a.__add__(b). If that returns NotImplemented, Python then tries b.__radd__(a).

__add__ — Left-side addition

Implements the binary + operator for self + other. Return the result of the operation, or return NotImplemented to signal that the operation is not supported.

class Vector:
    """A 2D vector with x and y components."""

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)
# output: Vector(4, 6)

Return NotImplemented, not an exception. Raising TypeError short-circuits the protocol and Python will not then try other.__radd__(self).

__radd__ — Right-side (reflected) addition

Python calls __radd__ as a fallback when other.__add__(self) returns NotImplemented. The evaluation chain for a + b is:

  1. a.__add__(b) — tried first
  2. If it returns NotImplemented, then b.__radd__(a)

A common use case is allowing int + MyClass to work when int.__add__ does not know how to add a MyClass instance. The typical implementation delegates back to __add__:

class Money:
    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):
        if isinstance(other, Money):
            return Money(self.amount + other.amount)
        if isinstance(other, int):
            return Money(self.amount + other)
        return NotImplemented

    def __radd__(self, other):
        return self.__add__(other)

    def __repr__(self):
        return f"${self.amount}"

print(Money(10) + 5)   # output: $15
print(5 + Money(10))   # output: $15

__radd__ is not called automatically for every other + self. Python only calls it when the left operand’s __add__ explicitly returns NotImplemented. If both operands define __add__ but neither knows about the other, Python raises TypeError.

__iadd__ — In-place addition

Implements the += operator for self += other. For mutable objects, this should mutate self in place and return self. For immutable objects, returning a new object is acceptable.

class Counter:
    def __init__(self, count=0):
        self.count = count

    def __iadd__(self, other):
        self.count += other
        return self

    def __repr__(self):
        return f"Counter({self.count})"

c = Counter(10)
c += 5
print(c)
# output: Counter(15)

Must return self. Forgetting to return from __iadd__ binds the variable to None after +=, because the augmented assignment statement performs an assignment after calling the dunder method.

If __iadd__ is not defined, Python falls back to __add__ to implement +=, which creates a new object. For mutable types, defining __iadd__ avoids unnecessary copies.

Gotchas

Return NotImplemented, not an exception.

Inside __add__ or __radd__, never raise TypeError to indicate an unsupported type. Raise nothing — return NotImplemented. Raising an exception prevents Python from trying the reflected method:

class Bad:
    def __add__(self, other):
        raise TypeError("unsupported")   # wrong — prevents __radd__

# Python will NOT call __radd__ on the other operand

__iadd__ must return self.

Omitting the return causes surprising behaviour:

class Broken:
    def __iadd__(self, other):
        self.value += other
        # missing return

b = Broken()
b += 10
print(b)   # output: None — b is now None

Mutable default trap.

Using a mutable object as a default argument shares that object across all instances:

class BrokenList:
    def __init__(self, data=[]):
        self.data = data

    def __iadd__(self, other):
        self.data.append(other)
        return self

a = BrokenList()
a += 1
b = BrokenList()
print(b.data)   # output: [1] — shared state between a and b

Use data=None and assign inside the body instead:

class FixedList:
    def __init__(self, data=None):
        self.data = data if data is not None else []

    def __iadd__(self, other):
        self.data.append(other)
        return self

See Also