When does overloading a Python operator hurt more than a named method?
answer
- Borrowed meaning, not invented meaning
- Short syntax can hide long work
- Try grepping for a plus sign
- In place means everyone's copy
- A verb explains itself
basics
~20 sOverload only when the operator has one obvious meaning for the type, such as arithmetic on a value object. If a reader must look up what + does, or the operator hides mutation of shared state, a named method is clearer.
solid answer
~50 sThe test is whether the operator's conventional meaning survives unchanged. Good candidates are value-like types where `+`, `-` or `<` already mean something, the result type is predictable, and `+` yields a new object while `+=` mutates — the expectation everyone brings from lists and strings. It goes wrong when the operator is a private joke for a domain verb, when it hides cost so a `+` that copies a 6,800-row batch reads as free, or when it hides mutation: an in-place `__iadd__` on a shared object changes it for every caller, and because augmented assignment reads, operates and stores back, two threads doing `digest += rows` can lose one another's work. Operators are also hard to grep, and tracebacks name dunders rather than intent. `digest.add_rows(rows)`, with a `threading.Lock` inside if it is shared, says more and costs nothing.
code
python · 16 linesclass Digest:
def __init__(self, rows=()):
self.rows = list(rows)
def __add__(self, other):
return Digest(self.rows + other.rows)
def add_rows(self, rows):
self.rows.extend(rows)
return self
a = Digest(["r1", "r2"])
b = Digest(["r3"])
print(len((a + b).rows), len(a.rows))
print(len(a.add_rows(["r4"]).rows))go deeper
Remember the baseline convention: + should build a new object and leave both operands alone, while += may change the left one. Do not invent operator meanings for domain actions.
Explain the criteria you would apply before defining an operator dunder — one obvious meaning, predictable return type, honest about mutation — and give an example of a type that qualifies and one that does not.
Show production consequences: hidden copying cost, mutation reaching callers through an alias, non-atomic augmented assignment on shared state, and the debugging tax of tracebacks and greps that name dunders instead of intent.
Own it as an API policy. Decide which of your types are algebraic value objects that may carry operators and which are builders or services that expose verbs only, and make immutability the default so shared state is not mutable through an operator at all.
## Operators are a vocabulary you did not invent Every operator arrives at your class carrying meaning the reader learned elsewhere. `+` combines two values of a kind into a third of the same kind; `<` orders; `in` tests membership; `*` scales or repeats. Overloading works when your type genuinely joins that vocabulary — money, vectors, matrices, durations, sets, paths, version numbers. It fails when the operator is repurposed as shorthand for a domain verb, because the reader has no way to guess and no obvious place to look. A usable four-part test before defining an operator dunder: 1. **Is there exactly one meaning a reader would guess?** If two are plausible — does `digest_a + digest_b` merge recipients or concatenate rows? — the operator is already the wrong tool. 2. **Does it respect the mutation convention?** `a + b` must produce a new object and leave both operands alone; `a += b` may mutate the left one. A `+` that mutates violates an expectation absolutely everyone holds. 3. **Is the return type predictable?** Same-kind in, same-kind out. An operator that sometimes returns a different class turns type checking and downstream code into guesswork. 4. **Does it improve the call site?** `total = price * quantity + shipping` earns its keep. `report = sender << rows` does not. ## The three costs, concretely **Operators hide cost.** Syntax that short suggests something cheap. If `+` on your batch type copies a 6,800-row list every time, a reader writing it in a loop has no visual warning, whereas `batch.concat(rows)` at least prompts the question. Named methods let the name carry a cost signal: `copy_with`, `merged`, `extend`. **Operators hide mutation, and mutation travels.** A well-behaved `__iadd__` mutates in place and returns `self`, so every name bound to that object sees the change: ```python shared = Digest() alias = shared alias += ["r1"] print(shared.rows) # ['r1'] — the alias changed the shared object ``` That is correct Python and a real defect source, because the syntax reads like a local assignment to `alias`. Worse, augmented assignment is a read-operate-store sequence rather than one step, so when an email-digest sender lets two worker threads run `digest += rows` against one shared builder, an update can be computed from a value another thread has already replaced, and rows go missing. The operator did not create the hazard, but it made it invisible. `digest.add_rows(rows)`, taking a `threading.Lock` internally, both names the mutation and gives you somewhere to put the guard. **Operators are hard to find.** You can grep `.add_rows(` and enumerate every caller in a minute; you cannot grep `+` usefully. Stack traces name `__iadd__` rather than the operation, IDE call hierarchies lose the edge, and a reviewer scanning a diff sees a one-character change with a large blast radius. ## What to write instead The fix is rarely "no operators anywhere". It is to split the type's surface: keep operators for the small, algebraic, immutable core, and expose everything else as named methods. A value type that is immutable can define `+` safely and skip in-place operators entirely, so callers get a new object and nobody's alias moves under them. A mutable builder should expose `add_rows`, `merge_from`, `reset` — verbs — and may define no operators at all. When you do overload, document the meaning in the class docstring, keep the operand types tight, and return `NotImplemented` for operands you do not understand so Python raises a clear `TypeError` instead of your class inventing an answer. ## The judgement an interviewer is listening for Weak answers reduce to a slogan ("operator overloading is bad" or "it makes code Pythonic"). Strong answers give criteria and a cost model: operators are for types whose meaning is already algebraic; the price is discoverability, hidden cost and hidden mutation; the price is worth paying for a `Money` or a `Vector` and almost never for a service object, a builder or anything shared across threads. The best answers add a review heuristic — if the pull request has to explain what the operator does, the method name would have explained it for free.
- Your wrapper type holds an integer count — which method makes it usable as a list index, and why is `__int__` not enough?`__index__`. Indexing, slicing, `range`, `hex`, `bin`, `oct` and `operator.index` all require a *lossless* integer conversion, and that is exactly what `__index__` promises; `__int__` may be lossy, as it is for `float`, so it is not accepted there. Defining `__index__` also gives you `int()` for free, since `int()` falls back to it when `__int__` is absent.
- What does defining `__bool__` on a value class buy you?Control over truth testing. `if obj:`, `while obj:`, `not obj` and boolean short-circuits all consult it, and by default every instance is truthy — so a type that can be empty or zero must say so itself or it will silently pass every `if`. The method has to return an actual `bool`; returning an `int` raises `TypeError`.
- How would you make an accumulator safe when several threads add to it?Stop expressing the update as an operator. `+=` is a read, operate and store sequence, so it is not atomic no matter how the dunder is written. Either make the type immutable so `+` hands each caller its own new object, or expose a named method that takes a `threading.Lock` internally — which is also where a reviewer will look for the guard.
Operators are the punctuation of a codebase. A comma in the expected place is invisible and helpful; a semicolon repurposed to mean "send this email" makes every reader stop and look it up.
saying these in an interview costs you the question
- Overloads `+` for an action with no arithmetic meaning
- Writes a `+` that mutates its left operand
- Assumes `x += y` is atomic across threads
- Adds operators for elegance rather than for the reader
- Thinks operator syntax implies a cheap operation
- Returns an unrelated type from an overloaded operator