Python Operator Overloading: Assignment Operators

Assignment operators like +=, -=, and *= are commonly used in Python to update a variable by applying an operation directly to it. These operators perform what’s called in-place operations, meaning they try to modify the object itself instead of creating a new one.

Python gives you the power to customize how these assignment operators work in your own classes using special methods like __iadd__, __isub__, and so on. By overloading these, you can make your custom objects behave more like built-in types — natural, readable, and expressive.

For example, imagine a BankAccount class:

account = BankAccount(100)
account += 50  # Instead of account.balance = account.balance + 50

Or an Inventory system:

inventory = Inventory({'apple': 10})
inventory += {'banana': 5}

These overloaded assignment operators make your classes more intuitive and easier to use in real-world scenarios — like managing bank balances, merging item lists, growing statistics, or handling permissions.

The Assignment Operators and Their Methods

Python provides special methods to handle in-place operations, allowing your objects to update themselves when using assignment operators like += or *=. These methods are called in-place operator methods and are listed below:

OperatorMethodDescription
+=__iadd__In-place addition
-=__isub__In-place subtraction
*=__imul__In-place multiplication
/=__itruediv__In-place true division
//=__ifloordiv__In-place floor division
%=__imod__In-place modulo
**=__ipow__In-place exponentiation
@=__imatmul__In-place matrix multiplication
&=__iand__In-place bitwise AND
|=__ior__In-place bitwise OR
^=__ixor__In-place bitwise XOR
<<=__ilshift__In-place left shift
>>=__irshift__In-place right shift

These methods define how your class should react when it’s modified directly with an assignment operator. For example, with __iadd__, you can control what happens when someone does obj += value.

The term “in-place” means the object is changed directly, rather than creating and returning a new object. If your class supports in-place changes (like updating an internal list or numeric value), then overloading these methods helps make your code cleaner, faster to write, and more expressive.

In-Place Addition with __iadd__ (+=)

The __iadd__ method in Python lets you define how your object should behave when using the += operator. Instead of returning a new object, this method updates the current object in place. This is especially helpful when working with things like counters, accumulators, or any stateful object you want to change directly.

Let’s look at a fun example: a BankAccount class. Each account has an owner and a balance. Using +=, we want to add money to the account balance in a natural and expressive way.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def __iadd__(self, amount):
        self.balance += amount
        return self

    def __repr__(self):
        return f"{self.owner}'s account: ${self.balance}"

# Example usage
account = BankAccount("Edward", 100)
print(account)         # Edward's account: $100

account += 50
print(account)         # Edward's account: $150

account += 25
print(account)         # Edward's account: $175

In the code above, the __iadd__ method takes amount and adds it to the current balance. Then it returns self so the object remains usable. When we do account += 50, it automatically calls __iadd__, and the balance increases by 50. This works again with += 25, adding more to the same object. This pattern lets your objects act more like built-in types, making your code cleaner and easier to follow.

In-Place Subtraction with __isub__ (-=)

Just like __iadd__ lets us add to an object with +=, the __isub__ method defines how an object handles -=. This is useful when you want to subtract a value and update the object itself.

Let’s build on the previous BankAccount example. We’ll add the __isub__ method so that we can subtract money directly using -=—like spending or withdrawing funds.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def __iadd__(self, amount):
        self.balance += amount
        return self

    def __isub__(self, amount):
        self.balance -= amount
        return self

    def __repr__(self):
        return f"{self.owner}'s account: ${self.balance}"

# Example usage
account = BankAccount("Edward", 200)
print(account)         # Edward's account: $200

account -= 30
print(account)         # Edward's account: $170

account -= 20
print(account)         # Edward's account: $150

In this version, the __isub__ method subtracts the given amount from the current balance and then returns the same object. Each time we use -=, the balance is updated right away, and there’s no need to reassign or create a new object.

This pattern is great for objects that represent changing values, like bank accounts, inventory, or timers.

In-Place Multiplication with __imul__ (*=)

The __imul__ method is used to define how an object handles in-place multiplication with *=. This is useful when you want to multiply internal values and store the result back into the object.

Let’s say you run a shop and keep track of item quantities in an Inventory class. If a new shipment triples your stock, *= is a clean and readable way to update your inventory.

Here’s how that works:

class Inventory:
    def __init__(self, item, quantity):
        self.item = item
        self.quantity = quantity

    def __imul__(self, factor):
        self.quantity *= factor
        return self

    def __repr__(self):
        return f"{self.item}: {self.quantity} units"

# Example usage
stock = Inventory("Basketballs", 10)
print(stock)         # Basketballs: 10 units

stock *= 3
print(stock)         # Basketballs: 30 units

stock *= 2
print(stock)         # Basketballs: 60 units

In this example, __imul__ multiplies the quantity by the factor and returns the same object. It’s a simple and clear way to scale up inventory, like during restocking or promotional events.

In-Place True Division with __itruediv__ (/=)

The __itruediv__ method lets you define what happens when your object is divided in-place using /=. This is helpful when you want to scale something down — like adjusting a measurement.

Imagine a Measurement class used to track recipe quantities. If you want to reduce a recipe to half or a third, using /= makes it feel natural and intuitive.

Here’s a simple example:

class Measurement:
    def __init__(self, ingredient, amount):
        self.ingredient = ingredient
        self.amount = amount  # in grams

    def __itruediv__(self, divisor):
        self.amount /= divisor
        return self

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

# Example usage
sugar = Measurement("Sugar", 300)
print(sugar)          # Sugar: 300g

sugar /= 2
print(sugar)          # Sugar: 150.0g

sugar /= 3
print(sugar)          # Sugar: 50.0g

In this code, each time we use /=, it modifies the amount in place, keeping the object the same but updating its value. This makes scaling recipes or adjusting unit quantities straightforward and Pythonic.

In-Place Floor Division with __ifloordiv__ (//=)

The __ifloordiv__ method lets you define what happens when your object is floor-divided using //=. Floor division discards the decimal part, giving you whole-number results.

A practical example is an Inventory class where you might want to split total items into containers. Using //= updates the total count to reflect how many full containers you can fill.

Here’s how that might look:

class Inventory:
    def __init__(self, item, quantity):
        self.item = item
        self.quantity = quantity

    def __ifloordiv__(self, size):
        self.quantity //= size
        return self

    def __repr__(self):
        return f"{self.item}: {self.quantity} full containers"

# Example usage
apples = Inventory("Apples", 115)
print(apples)          # Apples: 115 full containers

apples //= 10
print(apples)          # Apples: 11 full containers

apples //= 2
print(apples)          # Apples: 5 full containers

In this example, each use of //= updates the inventory by dividing and rounding down. It’s useful when working with packaging, batching, or other whole-unit constraints.

In-Place Modulo with __imod__ (%=)

The __imod__ method lets your object handle the %= operator. This is useful when you want to keep a value within a certain range — like hours on a clock or positions in a loop.

Let’s use a simple Timer class that wraps time values using %=. This can help reset the time to always stay within 60 seconds or 24 hours, for example.

class Timer:
    def __init__(self, seconds):
        self.seconds = seconds

    def __imod__(self, limit):
        self.seconds %= limit
        return self

    def __repr__(self):
        return f"Timer({self.seconds} sec)"

# Example usage
t = Timer(75)
print(t)       # Timer(75 sec)

t %= 60
print(t)       # Timer(15 sec)

t.seconds += 120
t %= 60
print(t)       # Timer(15 sec)

In this example, %= keeps the timer value wrapped around 60 seconds. It’s a great way to simulate behavior like digital clocks, countdowns, or bounded counters.

In-Place Exponentiation with __ipow__ (**=)

The __ipow__ method handles the **= operator, letting you update an object’s value by raising it to a power, right in place. This is perfect for models where values grow exponentially over time.

Imagine a simple Growth class that tracks population size or investment value. Using **= lets you update the amount by raising it to a power directly.

class Growth:
    def __init__(self, value):
        self.value = value

    def __ipow__(self, power):
        self.value **= power
        return self

    def __repr__(self):
        return f"Growth({self.value})"

# Example usage
g = Growth(2)
print(g)      # Growth(2)

g **= 3
print(g)      # Growth(8)

g **= 2
print(g)      # Growth(64)

In this example, the Growth object’s value increases by raising it to the given power. Using **= with __ipow__ keeps the code clean and directly changes the object, perfect for exponential growth scenarios.

In-Place Matrix Multiplication with __imatmul__ (@=)

The __imatmul__ method overloads the @= operator, which performs in-place matrix multiplication. This operator is widely used in fields like data science, computer graphics, and machine learning for multiplying matrices efficiently and clearly.

Imagine a simple Matrix class that holds 2D lists. By defining __imatmul__, we let a matrix multiply itself by another matrix right in place, updating its data without creating a new object.

class Matrix:
    def __init__(self, data):
        self.data = data  # data is a list of lists

    def __imatmul__(self, other):
    
        result = []
        
        for i in range(len(self.data)):
        
            row = []
            
            for j in range(len(other.data[0])):
            
                sum_val = 0
                
                for k in range(len(other.data)):
                    sum_val += self.data[i][k] * other.data[k][j]
                    
                row.append(sum_val)
                
            result.append(row)
            
        self.data = result
        
        return self

    def __repr__(self):
        return f"Matrix({self.data})"

# Example usage
m1 = Matrix([[1, 2], [3, 4]])
m2 = Matrix([[2, 0], [1, 2]])

print("Before @= : ", m1)

m1 @= m2

print("After @= : ", m1)

Before the @= operation, m1 holds its original matrix. After m1 @= m2, it updates itself with the matrix product of m1 and m2. This in-place multiplication is useful in algorithms where you want to keep updating a matrix without making extra copies, making your code more efficient and readable.

Bitwise Assignment Operators

Bitwise assignment operators are useful when you want to modify flags or bits in place. Let’s use a BitFlag class to demonstrate each one clearly.

__iand__ (&=): Keep Only Shared Flags

The &= operator keeps only the bits that are set in both objects. It’s useful to find common permissions or flags.

class BitFlag:
    def __init__(self, flags):
        self.flags = flags

    def __iand__(self, other):
        self.flags &= other.flags
        return self

    def __repr__(self):
        return f"BitFlag({bin(self.flags)})"

# Example
flag1 = BitFlag(0b1101)
flag2 = BitFlag(0b1011)

flag1 &= flag2  # Only bits common to both remain: 0b1001
print(flag1)

This example shows how flag1 updates itself to keep only the bits it shares with flag2. After the operation, flag1 holds the value 0b1001, representing only the common bits. This is useful when you want to narrow down to permissions or flags that both objects agree on.

__ior__ (|=): Combine Multiple Flags

The |= operator sets bits that appear in either object, combining permissions or options.

class BitFlag:
    def __init__(self, flags):
        self.flags = flags

    def __ior__(self, other):
        self.flags |= other.flags
        return self

    def __repr__(self):
        return f"BitFlag({bin(self.flags)})"

# Example continued
flag1 = BitFlag(0b1001)
flag3 = BitFlag(0b0100)

flag1 |= flag3  # Combines bits: 0b1101
print(flag1)

Here, flag1 expands to include all bits set in either itself or flag3. The resulting 0b1101 means the combined flags now cover both sets. This is handy for merging multiple permissions or feature flags together.

__ixor__ (^=): Toggle Flags

The ^= operator flips bits that differ between the two objects, useful for toggling options.

class BitFlag:
    def __init__(self, flags):
        self.flags = flags

    def __ixor__(self, other):
        self.flags ^= other.flags
        return self

    def __repr__(self):
        return f"BitFlag({bin(self.flags)})"

# Example continued
flag1 = BitFlag(0b1101)
flag2 = BitFlag(0b1011)

flag1 ^= flag2  # Toggles bits: 0b0110
print(flag1)

In this toggle example, bits that are different between flag1 and flag2 flip. The result 0b0110 shows which bits were changed. This is perfect when you want to turn flags on or off depending on another set of flags.

__ilshift__ (<<=): Shift Bits Left

Shifts all bits left by a certain number of places, increasing the “level” or value of flags.

class BitFlag:
    def __init__(self, flags):
        self.flags = flags

    def __ilshift__(self, count):
        self.flags <<= count
        return self

    def __repr__(self):
        return f"BitFlag({bin(self.flags)})"

# Example continued
flag1 = BitFlag(0b0100)

flag1 <<= 1  # Shift bits left: 0b1000
print(flag1)

Shifting bits left moves all bits towards higher positions, effectively multiplying the value by powers of two. This can represent increasing permission levels or moving flags into new categories.

__irshift__ (>>=): Shift Bits Right

Shifts bits right, lowering values or moving to less significant bits.

class BitFlag:
    def __init__(self, flags):
        self.flags = flags

    def __irshift__(self, count):
        self.flags >>= count
        return self

    def __repr__(self):
        return f"BitFlag({bin(self.flags)})"

# Example continued
flag1 = BitFlag(0b1000)

flag1 >>= 2  # Shift bits right: 0b0010
print(flag1)

Here, bits move to lower positions, dividing the value by powers of two and often representing a downgrade or lessening of permissions or levels.

Each operator modifies the flags in-place, making your bit flag manipulations clean, natural, and easy to read. This pattern fits real-world uses like permission systems, feature toggles, or low-level data control.

Inventory Class With All Assignment Operators

Here’s a full example combining many assignment operators into a single Inventory class. This shows how these operators make code cleaner and more natural when managing real-world data like item stocks:

class Inventory:
    def __init__(self, items):
        # items is a dictionary: {item_name: quantity}
        self.items = items

    def __iadd__(self, other):
        # Add quantities from another Inventory
        for item, qty in other.items.items():
            self.items[item] = self.items.get(item, 0) + qty
        return self

    def __isub__(self, other):
        # Subtract quantities from another Inventory
        for item, qty in other.items.items():
            self.items[item] = self.items.get(item, 0) - qty
        return self

    def __imul__(self, multiplier):
        # Multiply all quantities by an integer
        for item in self.items:
            self.items[item] *= multiplier
        return self

    def __itruediv__(self, divisor):
        # Divide all quantities by a number (float division)
        for item in self.items:
            self.items[item] /= divisor
        return self

    def __imod__(self, divisor):
        # Modulo each quantity, useful for batch splits
        for item in self.items:
            self.items[item] %= divisor
        return self

    def __repr__(self):
        return f"Inventory({self.items})"

# Example usage
inventory1 = Inventory({'apple': 10, 'banana': 5})
inventory2 = Inventory({'apple': 3, 'orange': 7})

print("Initial inventory1:", inventory1)
print("Initial inventory2:", inventory2)

inventory1 += inventory2
print("After adding inventory2:", inventory1)

inventory1 -= Inventory({'banana': 2, 'orange': 1})
print("After subtracting some items:", inventory1)

inventory1 *= 2
print("After doubling quantities:", inventory1)

inventory1 /= 3
print("After dividing quantities by 3:", inventory1)

inventory1 %= 4
print("After modulo 4 on quantities:", inventory1)

This example shows how the assignment operators (+=, -=, *=, /=, %=) allow you to update your inventory data directly and clearly. Instead of writing long loops every time, you just use natural operators, making your code easier to read and maintain. This pattern fits many real apps like managing stock, adjusting balances, or scaling values smoothly.

Conclusion

Overloading assignment operators lets your custom classes behave more like built-in Python types, making your code cleaner and easier to read. By defining methods like __iadd__, __isub__, and others, you enable natural, in-place updates that feel intuitive when using operators like += or *=.

This approach is especially useful for classes that handle numbers, collections, or flags—like BankAccount, Inventory, or BitFlag. Experimenting with these operators in your own projects will help you write code that feels smooth and expressive, just like Python’s built-in objects.