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:
Operator | Method | Description |
---|---|---|
+= | __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.