Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

User-defined arithmetic operators

Classes and user-defined enums can implement arithmetic with Python-style special methods. The compiler selects the method statically and emits an ordinary direct call. There is no runtime lookup, boxing, or allocation for dispatch.

OperatorLeft methodReflected methodIn-place method
+__add____radd____iadd__
-__sub____rsub____isub__
*__mul____rmul____imul__
/__truediv____rtruediv____itruediv__
//__floordiv____rfloordiv____ifloordiv__
%__mod____rmod____imod__
&__and____rand____iand__
|__or____ror____ior__
^__xor____rxor____ixor__
<<__lshift____rlshift____ilshift__
>>__rshift____rrshift____irshift__

Unary +, -, and ~ call __pos__, __neg__, and __invert__. Precedence and associativity are unchanged.

Signatures and resolution

A value-producing hook has a shared self, exactly one other parameter for binary operators, and no other parameters for unary operators. The other operand is a shared reference or copyable value. Its type can differ from the receiver’s. The result is an owned value of any supported return type, including Result; unit and reference results are rejected. Hooks cannot declare method type parameters, parameter defaults, or yield. Generic classes and ordinary generic functions can use operators after specialization.

For a + b, the compiler selects a.__add__ if declared; otherwise it selects b.__radd__ if declared. A selected hook’s signature must match: a type error does not cause fallback. There is no dynamic NotImplemented protocol, subtype priority, or implicit conversion. A reflected method is allowed for operands of the same type when the left method is absent. Ordinary member visibility applies: exported APIs need pub hooks.

Both operands are evaluated exactly once, left to right, even for a reflected call. Shared operands stay borrowed through the call. Temporary owners survive the full expression and are destroyed normally; no implicit deep copy occurs. Methods can still be called explicitly.

An operation that allocates should return Result, just as a named method does. Write (a + b)? to propagate failure: ? binds more tightly than +. The operator syntax does not automatically propagate, unwrap, or discard errors.

In-place operations

target += value calls target.__iadd__(value); the other in-place names follow the table. Its signature requires self: &mut T, one shared or copyable operand, and -> (). It mutates the existing destination and does not replace it with a returned owner. The target must be a writable binding, field, collection element, or exclusive reference. Its indices are evaluated once before the right side. The ordinary mutable-method receiver reservation prevents invalidating that place while evaluating arguments.

There is no fallback from += to +. For an operation that creates a new value or can fail, use explicit assignment (a = (a + b)?) or a named fallible method. This keeps allocation and error handling visible. An in-place hook can perform explicitly handled allocation inside its body; dispatch itself allocates nothing.

Newtype forwarding

A class annotated @newtype wraps exactly one field and forwards every operator that the field’s type supports, so a wrapper keeps its field’s arithmetic without one hook per operator:

@newtype
class Metres:
    value: i64

def main() -> Result[(), IoError | OverflowError[i64]]:
    mut total = Metres(3) + Metres(4)
    total *= Metres(2)
    print(total, -total, total > Metres(10))?
    Ok(())
Metres(value=14) Metres(value=-14) True

a + b on two Metres values lowers to a.value + b.value through the ordinary operator rules for the field type, then wraps the result:

  • A result of the field’s type becomes the class: Metres + Metres is a Metres.
  • A Result[F, E] result, where F is the field type, becomes Result[Class, E]. @newtype class Name: text: str gives Name + Name -> Result[Name, AllocError], written (a + b)?.
  • Any other result is returned as is, such as the f64 from a field hook __mul__(self, other: &Vec2) -> f64.
  • <, <=, >, and >= forward and return bool. == and != stay structural, as for every class.
  • Unary +, -, and ~ forward in the same way.
  • target += value performs the field’s own in-place operation, target.value += value.value, so it follows the field type’s +=.

Forwarding happens only when no hook is selected and both operands have the same @newtype class. Typing stays nominal: Metres + i64, i64 + Metres, and Metres + Feet are errors unless a hook accepts them; there is no implicit conversion. An explicit hook replaces forwarding for its operator only: a @newtype class with __add__ uses it for + and still forwards -. Reflected hooks apply only to mixed operand types, so they never compete with forwarding.

Each operand is evaluated once, left to right, and borrowed as a hook operand would be; forwarding never moves or copies the wrapper. Overflow errors, division rules, allocation failure, and other errors are those of the wrapped operation. An operator the field type rejects is rejected with the field type’s diagnostic: a list[i64] wrapper has no +, and an f64 wrapper has no %. Forwarding allocates nothing beyond what the wrapped operation does.

Forwarded operations build the result through the class constructor, which is why a @newtype class cannot define __init__. The declaration rules are in annotations.

Equality, ordering and hashing

Equality, ordering and hashing never call user code:

  • == and != compare fields structurally, including inside collections.
  • <, <=, > and >= on a class come only from newtype forwarding.
  • Dictionary keys and set elements use built-in structural hashing. Classes are not keys yet; named enums with supported key payloads are accepted.

A class, enum or protocol that declares __eq__, __ne__, __lt__, __le__, __gt__, __ge__, __cmp__, __hash__ or __key__ is rejected, so a Python-style hook cannot compile and then be ignored. Rename a method that is not meant as a hook, or compare explicitly:

class Money:
    cents: i64

    def same_amount(self, other: &Money) -> bool:
        self.cents == other.cents

def main() -> Result[(), IoError]:
    print(Money(1) == Money(2), Money(1).same_amount(&Money(1)))?
    Ok(())
False True

User-defined ordering through one __cmp__ hook is planned under B37, and user-defined keys under B74; see the ordering and hashing proposals.

Reserved method names

A method named __name__, with two underscores on each side, must be a hook the compiler calls: an operator hook in the tables above, or, on a class, __init__, __del__, __enter__ or __exit__. A class, enum or protocol that declares any other such name is rejected, so a Python special method cannot compile and then be ignored. The names stay free for hooks the language may add. Where Plenty has its own form, the diagnostic names it:

Python methodPlenty form
__str__, __repr__, __format__display and debug methods
__pow__a pow method, called as base.pow(exponent)
__iter__, __next__a generator method
__len__, __contains__an ordinary shared len method for len(value); explicit search methods for membership
__bool__a predicate method such as is_empty
__getitem__, __setitem__named methods such as get and set

len(value) calls an ordinary shared len(self) -> i64 method on classes and enums. Membership through in and not in still uses only intrinsic operations; user-container membership remains under B49. A name with underscores on one side only, such as __size, is an ordinary method.

Boundaries

Custom truthiness, indexing, membership, assignment, dereference, **, and @ operators are not implemented; their Python hooks are reserved. and, or, and not keep their boolean and short-circuit rules. Numeric operators retain their overflow checks and division rules. Operators a newtype forwards to an integer field report OverflowError of the field type through the function’s declared Result by default, through a local checked: block, or as a violated precondition in unchecked:, just like the field’s own operators. A user-defined hook is an ordinary call with its own declared contract, independent of the caller’s mode; a fallible hook still needs explicit Result handling.

See the operator tutorial, assignment rules, and implementation and performance notes.