Operators
Python’s operator surface is similar to C’s at the level of arithmetic and comparison, with substantive differences: integer division (//) is distinct from true division (/), the power operator ** is built into the language, chained comparisons (a < b < c) read mathematically, and the boolean operators (and, or, not) are spelt as words rather than symbols. Operator overloading is admitted through dunder methods (__add__, __lt__, etc.); the dispatch model is type-driven and admits user-defined types to participate in arithmetic, comparison, and the various protocols. The walrus operator := (since 3.8) admits assignment within an expression.
This page covers the operator surface, precedence and associativity, the dunder dispatch model, and the conventional patterns.
Arithmetic operators
The principal arithmetic operators:
1 + 2 # 3
3 - 1 # 2
2 * 4 # 8
10 / 3 # 3.3333... (true division; always returns float)
10 // 3 # 3 (floor division; integer when both operands are int)
10 % 3 # 1 (remainder)
2 ** 10 # 1024 (exponentiation)
divmod(10, 3) # (3, 1) (quotient and remainder)
-x # negation
+x # identity (unary plus)
abs(-5) # 5
The two division operators are the principal Python-specific feature:
/— true division: always returnsfloat.10 / 2is5.0, not5.//— floor division: returnsintwhen both operands areint, otherwisefloat. Rounds toward negative infinity (so-7 // 2is-4, not-3).
The two were unified before Python 3 (and the change broke a substantial body of code); modern Python distinguishes them clearly.
The ** operator is exponentiation; right-associative (2 ** 3 ** 2 is 2 ** 9, not 8 ** 2).
Comparison operators
The principal comparison operators:
a == b # equality
a != b # inequality
a < b # less than
a <= b # less than or equal
a > b # greater than
a >= b # greater than or equal
a is b # identity (same object)
a is not b # not same object
a in b # membership
a not in b # non-membership
Comparisons return bool. Several distinctive features:
Chained comparisons
0 < x < 10 # equivalent to (0 < x) and (x < 10)
a == b == c # equivalent to (a == b) and (b == c)
1 < 2 == 2 < 3 # all of (1 < 2), (2 == 2), (2 < 3)
Each operand is evaluated at most once; the chain short-circuits on the first false.
is versus ==
is tests identity (two references to the same object); == tests equality (two values that compare equal):
a = [1, 2, 3]
b = [1, 2, 3]
c = a
a == b # True (equal contents)
a is b # False (distinct list objects)
a is c # True (same object)
a is None # the conventional None test
None == None # True, but the convention is `is None`
The conventional discipline:
- Use
isforNone,True,False, and the singletons. - Use
==for value comparison.
For small integers and short strings, CPython interns the objects, so is may “work” by accident; the conventional advice is not to rely on it.
Membership
3 in [1, 2, 3] # True
"a" in "abc" # True
"key" in {"key": 1} # True (dict)
1 in {1, 2, 3} # True (set)
The in operator works on any container — lists, tuples, sets, dicts (checks keys), strings (checks substrings). For dict, in checks keys, not values.
Boolean operators
Three logical operators, spelt as words:
True and False # False (short-circuiting)
True or False # True (short-circuiting)
not True # False
Important: and and or return one of their operands, not necessarily a bool:
1 and 2 # 2 (last truthy)
0 or "default" # "default" (first truthy)
None or "fallback" # "fallback"
[1, 2] and [3, 4] # [3, 4]
[] or "empty" # "empty"
The mechanism admits the conventional Python idioms:
result = config.get("key") or "default" # default if key is missing or falsy
display = name and name.upper() # uppercase if name is truthy
The and/or return a useful value, not just True/False.
Bitwise operators
The principal bitwise operators (on integers):
0b1010 & 0b1100 # 0b1000 (AND)
0b1010 | 0b1100 # 0b1110 (OR)
0b1010 ^ 0b1100 # 0b0110 (XOR)
~0b1010 # -0b1011 (bitwise NOT, two's complement)
0b1010 << 2 # 0b101000 (left shift)
0b1010 >> 1 # 0b0101 (right shift)
Python integers are arbitrary-precision; bitwise operations work on the (notional) two’s-complement representation. The ~ operator produces a negative number for non-negative operands because of the two’s-complement signing.
The same operators apply to set (and frozenset):
{1, 2, 3} | {3, 4, 5} # {1, 2, 3, 4, 5} (union)
{1, 2, 3} & {3, 4, 5} # {3} (intersection)
{1, 2, 3} - {2} # {1, 3} (difference)
{1, 2, 3} ^ {3, 4, 5} # {1, 2, 4, 5} (symmetric difference)
For dict, | is merge (since 3.9):
{"a": 1, "b": 2} | {"b": 3, "c": 4} # {"a": 1, "b": 3, "c": 4}
Assignment
Simple assignment uses =:
x = 42
y, z = 1, 2 # tuple unpacking
a, *rest = [1, 2, 3, 4] # a = 1, rest = [2, 3, 4]
Augmented assignments (compound operators):
x += 1 # equivalent to x = x + 1
x -= 1
x *= 2
x /= 2
x //= 2
x %= 2
x **= 2
x &= mask
x |= flag
x ^= 1
x <<= 1
x >>= 1
The augmented assignment dispatches through __iadd__, __isub__, etc., which admit in-place modification for mutable types:
xs = [1, 2, 3]
ys = xs
xs += [4, 5] # xs is modified in place; ys also reflects the change
For lists, += extends in place; + produces a new list. The distinction matters when shared references are involved.
The walrus operator :=
Python 3.8 introduced the walrus operator — an assignment expression:
if (n := len(data)) > 100:
print(f"too many: {n}")
while (line := input("> ")) != "quit":
process(line)
# In a list comprehension:
filtered = [y for x in data if (y := f(x)) > 0]
The walrus admits assignment within an expression; the assigned name is then usable in the surrounding context. The conventional uses:
- Capturing a value used in both a condition and the body (
if (n := len(data)) > 100). - Read-loops (
while (line := input()) != "quit"). - Comprehensions where the same computation is used twice.
The walrus is rare in routine code; the explicit = followed by if is conventionally clearer.
The conditional expression
Python’s ternary form:
result = "positive" if n > 0 else "non-positive"
The order is unconventional: the value-when-true comes first, then the condition, then the value-when-false. The grammar reads naturally: “this if cond else that”.
display = name if name else "anonymous"
sign = "+" if x > 0 else "-" if x < 0 else "0"
Nested ternaries are admitted but quickly become unreadable; the conventional fallback is an if/elif/else block.
Operator overloading via dunders
Python’s binary operators dispatch through dunder methods on the operands:
| Operator | Method | Notes |
|---|---|---|
+ | __add__, __radd__, __iadd__ | binary addition |
- | __sub__, __rsub__, __isub__ | binary subtraction |
* | __mul__, __rmul__, __imul__ | multiplication |
/ | __truediv__, __rtruediv__, __itruediv__ | true division |
// | __floordiv__, __rfloordiv__, __ifloordiv__ | floor division |
% | __mod__, __rmod__, __imod__ | modulo |
** | __pow__, __rpow__, __ipow__ | exponentiation |
== | __eq__ | equality |
< | __lt__ | less than |
<= | __le__ | less than or equal |
> | __gt__ | greater than |
>= | __ge__ | greater than or equal |
&, |, ^ | __and__, __or__, __xor__ (and __r…__, __i…__) | bitwise |
<<, >> | __lshift__, __rshift__ (and variants) | shift |
Unary - | __neg__ | negation |
Unary + | __pos__ | identity |
~ | __invert__ | bitwise complement |
abs | __abs__ | absolute value |
len | __len__ | length |
[i] | __getitem__, __setitem__, __delitem__ | subscript |
in | __contains__ | membership |
f(...) | __call__ | call |
bool(...) | __bool__ | truthiness |
str(...) | __str__ | string conversion |
repr(...) | __repr__ | representation |
hash | __hash__ | hashing |
For binary operators, the dispatch first tries a.__op__(b). If that returns NotImplemented, Python tries b.__rop__(a) (the reflected version). The mechanism admits user-defined types to participate in arithmetic with built-in types:
class Money:
def __init__(self, cents: int):
self.cents = cents
def __add__(self, other):
if isinstance(other, Money):
return Money(self.cents + other.cents)
return NotImplemented
def __mul__(self, factor):
if isinstance(factor, int):
return Money(self.cents * factor)
return NotImplemented
def __rmul__(self, factor):
return self.__mul__(factor) # admits 5 * money
m = Money(100)
m + Money(50) # Money with 150 cents
m * 3 # Money with 300 cents
3 * m # Money with 300 cents (via __rmul__)
The full treatment of dunders is in Duck typing and protocols.
Operator precedence
The full precedence table (selected, in order from highest to lowest):
| Level | Operators | Associativity |
|---|---|---|
| 1 | (...), [...], {...} | (grouping; not operators per se) |
| 2 | f(...), x[i], x.attr | left |
| 3 | await x | (n/a) |
| 4 | ** | right |
| 5 | +x, -x, ~x (unary) | right |
| 6 | *, /, //, %, @ | left |
| 7 | +, - | left |
| 8 | <<, >> | left |
| 9 | & | left |
| 10 | ^ | left |
| 11 | | | left |
| 12 | in, not in, is, is not, <, <=, >, >=, !=, == | non-associative (chained) |
| 13 | not x | right |
| 14 | and | left |
| 15 | or | left |
| 16 | if … else (conditional) | right |
| 17 | lambda | (n/a) |
| 18 | := (walrus) | (n/a) |
Three precedence facts worth memorising:
**binds tighter than unary-:-2 ** 2is-(2 ** 2) == -4, not(-2) ** 2 == 4.- Comparisons chain:
a < b < cis(a < b) and (b < c), not(a < b) < c. notbinds looser than the comparison operators:not a == bisnot (a == b), not(not a) == b.
When in doubt, parenthesise. Python does not penalise redundant parentheses.
A note on what Python does not have
The operators that some other languages provide and Python’s status:
| Operator / Feature | Available? |
|---|---|
Pre/post increment (++x, x++) | No. Use x += 1. |
| Pointer dereference / address-of | No. References are implicit. |
?: ternary | The if … else expression replaces it. |
&&, || | The word forms (and, or) replace them. |
<<< (unsigned right shift) | No. Use (x & 0xFFFFFFFF) >> n for fixed-width. |
| Custom operators | No (operators are fixed; dunders are predefined). |
The conventional Python style avoids operator-heavy expressions; the language admits the conventional arithmetic and comparison surface and uses named functions for everything else.