Polyglot
Languages Python operators
Python § operators

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 returns float. 10 / 2 is 5.0, not 5.
  • //floor division: returns int when both operands are int, otherwise float. Rounds toward negative infinity (so -7 // 2 is -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 is for None, 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:

OperatorMethodNotes
+__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):

LevelOperatorsAssociativity
1(...), [...], {...}(grouping; not operators per se)
2f(...), x[i], x.attrleft
3await x(n/a)
4**right
5+x, -x, ~x (unary)right
6*, /, //, %, @left
7+, -left
8<<, >>left
9&left
10^left
11|left
12in, not in, is, is not, <, <=, >, >=, !=, ==non-associative (chained)
13not xright
14andleft
15orleft
16if … else (conditional)right
17lambda(n/a)
18:= (walrus)(n/a)

Three precedence facts worth memorising:

  1. ** binds tighter than unary -: -2 ** 2 is -(2 ** 2) == -4, not (-2) ** 2 == 4.
  2. Comparisons chain: a < b < c is (a < b) and (b < c), not (a < b) < c.
  3. not binds looser than the comparison operators: not a == b is not (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 / FeatureAvailable?
Pre/post increment (++x, x++)No. Use x += 1.
Pointer dereference / address-ofNo. References are implicit.
?: ternaryThe if … else expression replaces it.
&&, ||The word forms (and, or) replace them.
<<< (unsigned right shift)No. Use (x & 0xFFFFFFFF) >> n for fixed-width.
Custom operatorsNo (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.