Python *= Operator: How It Works and When to Use It
python *= operator: Learn how the Python *= operator works, its in-place semantics for mutable types, and how to control it in your own classes.
The python *= operator is an augmented assignment that multiplies a variable by a value and assigns the result back. For example, x *= 2 is roughly equivalent to x = x * 2, but the exact behavior depends on the type of x. For mutable types, the operation may modify the object in place; for immutable types, it always creates a new object and rebinds the name. Understanding this distinction is essential for writing predictable code and for designing classes that behave intuitively.
Augmented Assignment Semantics
When Python encounters a *= b, it does not simply expand to a = a * b. Instead, it follows a specific lookup order. First, it checks whether the type of a defines __imul__. If it does, Python calls a.__imul__(b) and assigns the returned value back to a. If __imul__ is not defined, Python falls back to a.__mul__(b) and then rebinds a to that result. This is why the behavior can differ between mutable and immutable types.
The same pattern applies to other augmented assignment operators like +=, -=, and /=. Each operator has a corresponding in-place method (__iadd__, __isub__, etc.) that takes precedence over the regular binary method. The fallback ensures that even if a type does not implement the in-place variant, the operation still works, but it behaves like a normal multiplication followed by assignment.
In-Place vs Rebinding for Mutable and Immutable Types
The key difference lies in whether the object itself is modified or a new object is created. For immutable types—such as int, float, str, and tuple—there is no __imul__ method because the object cannot be changed. Python therefore always calls __mul__, creates a new object, and rebinds the variable. The original object remains unchanged, which is consistent with immutability.
For mutable types, the story is different. A list defines __imul__, which extends the list in place by repeating its elements. The variable still points to the same list object, but its contents have changed. This distinction matters when multiple variables reference the same object: mutating one will affect all references, whereas rebinding only affects the one variable.
Behavior with Common Built-in Types
Let's look at concrete examples for the most common built-in types.
# List: in-place mutation lst = [1, 2] original_id = id(lst) lst *= 2 print(lst) # [1, 2, 1, 2] print(id(lst) == original_id) # True
For a list, *= calls __imul__, which repeats the elements and modifies the list object itself. The identity remains the same.
# Tuple: creates a new tuple tup = (1, 2) original_id = id(tup) tup *= 2 print(tup) # (1, 2, 1, 2) print(id(tup) == original_id) # False
Tuples are immutable, so *= falls back to __mul__, producing a new tuple. The original tuple is unchanged.
# String: creates a new string s = "ab" s *= 3 print(s) # "ababab"
Strings are also immutable; *= creates a new string object.
# Integer: creates a new integer n = 5 n *= 3 print(n) # 15
Integers are immutable, so the variable is rebound to a new integer object.
Controlling Behavior in Custom Classes
If you are designing a class that represents a mutable container or a mathematical object, you may want to implement __imul__ to allow efficient in-place multiplication. Without it, Python will fall back to __mul__, which typically returns a new instance and may be less efficient.
Here is a simple example of a class that implements both methods:
class Vector: def __init__(self, values): self.values = list(values) def __mul__(self, scalar): return Vector([v * scalar for v in self.values]) def __imul__(self, scalar): for i in range(len(self.values)): self.values[i] *= scalar return self
With this implementation, v *= 2 modifies the existing Vector instance, while v = v * 2 creates a new Vector. This distinction is useful when you want to preserve object identity or avoid allocating a new object in performance-sensitive code.
Performance and Memory Considerations
In-place multiplication can reduce memory churn for mutable types. For a large list, lst *= n extends the list by reusing the existing allocation when possible, whereas lst = lst * n creates a completely new list and then reassigns the variable. The latter requires allocating a new list and copying all elements, which is more work. However, the exact performance gain depends on the implementation and the size of the data; it is not a guarantee of a specific speedup.
For immutable types, there is no in-place benefit because a new object is always created. The operation is equivalent to x = x * y in terms of memory usage and speed. If you are repeatedly multiplying a large immutable structure, consider whether a mutable alternative (like a list instead of a tuple) would be more appropriate for your use case.
When defining custom classes, implementing __imul__ can avoid unnecessary allocations. But do not over-optimize prematurely. Only add __imul__ when you have measured that object creation is a bottleneck or when in-place semantics are part of the class's contract.
Common Mistakes and Edge Cases
The most common mistake with *= is assuming that it always mutates the object in place. This is only true for types that implement __imul__. For immutable types, the variable is rebound, and any other references to the original object remain unchanged. Consider this aliasing scenario:
a = [1, 2] b = a a *= 2 print(b) # [1, 2, 1, 2]
Because a and b reference the same list, mutating a also changes b. If you intended to create a new list, you should use a = a * 2 instead.
Another edge case is using *= with incompatible types. For example, "abc" *= 3 works, but "abc" *= [1] raises a TypeError. The error message will indicate that a string cannot be multiplied by a list. Similarly, 3 *= "a" raises a TypeError because an integer does not support multiplication by a string.
Finally, augmented assignment is a statement, not an expression. You cannot write y = (x *= 2); this is a syntax error. The operator is designed to be used as a standalone statement, and trying to embed it in a larger expression will fail.