Skip to main content
Python · lesson 5 of 34
Python beginner Lesson 5 of 34

Python Operators

Run Python's arithmetic, comparison, identity, logical, membership, bitwise and walrus operators, plus operator overloading, and see each real result.

By SkillByExample · Updated

Operators are the symbols that combine values: +, ==, and, in, &, := and friends. Most behave as you’d expect; the examples below concentrate on the places Python surprises people. Output is from Python 3.13.

Arithmetic

a, b = 10, 3
print(a + b, a - b, a * b, a ** b)
print(a / b, 6 / 3)
print(a // b, -a // b)
print(a % b, -a % b)
print(2 ** 3 ** 2, (2 ** 3) ** 2)
print(0.1 + 0.2, 0.1 + 0.2 == 0.3)

x = 5
x += 3
x *= 2
x //= 3
print(x)
$ python arithmetic.py
13 7 30 1000
3.3333333333333335 2.0
3 -4
1 2
512 64
0.30000000000000004 False
5

/ returned a float even for 6 / 3. // rounds down, so -10 // 3 is -4, not -3, and % follows the sign of the divisor so that (a // b) * b + a % b == a always holds. ** groups right to left: 2 ** 3 ** 2 is 2 ** 9. And floats are binary fractions, so 0.1 + 0.2 is not exactly 0.3 — compare floats with math.isclose.

Precedence

When in doubt, the table answers it — but parentheses answer it for the reader too.

print(2 + 3 * 4, (2 + 3) * 4)
print(-2 ** 2, (-2) ** 2)
print(not 1 == 2)
print(1 + 2 << 1)
print(True or False and False)
$ python precedence.py
14 20
-4 4
True
6
True

-2 ** 2 is -(2 ** 2) because ** binds tighter than unary minus. not is lower than ==, so not 1 == 2 means not (1 == 2). Shifts are lower than +, and and binds tighter than or.

Precedence (high to low)Operators
exponent**
unary+x, -x, ~x
multiplicative*, /, //, %, @
additive+, -
shifts<<, >>
bitwise&, then ^, then |
comparisons==, !=, <, <=, >, >=, is, in and their not forms
booleannot, then and, then or
conditional, assignment expressionx if c else y, then :=

Comparisons and chaining

Comparisons can be chained, and each operand is evaluated only once.

x = 7
print(1 < x < 10, 0 <= x <= 5)
print(1 < x > 5)
print("apple" < "banana", "Zebra" < "apple")
print([1, 2, 3] < [1, 3])

try:
    print(3 < "4")
except TypeError as e:
    print("TypeError:", e)
$ python comparisons.py
True False
True
True True
True
TypeError: '<' not supported between instances of 'int' and 'str'

1 < x < 10 means 1 < x and x < 10. Chains don’t have to read like ranges — 1 < x > 5 is legal and true. Strings compare by code point, so uppercase "Zebra" sorts before lowercase "apple"; lists compare element by element. Unlike some languages, Python refuses to order unrelated types.

Identity (is) vs equality (==)

== compares values; is asks whether two names refer to the same object.

a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b, a is b, a is c)

c.append(4)
print(a)

value = None
print(value is None)

x = 1000
y = int("1000")
print(x == y, x is y)
$ python identity.py
True False True
[1, 2, 3, 4]
True
True False

a and b are equal but separate lists; c is another name for a, so appending through c changed a. The last line is why is must not be used for numbers or strings: two equal ints may or may not be the same object. Reserve is for singletons like None.

Logical operators return operands

and and or short-circuit, and they return one of their operands rather than a bool.

def loud(value):
    print(f"  evaluated {value!r}")
    return value

print(0 or "default")
print("" and "never")
print([1] and "second")

print("and:", loud(False) and loud(True))
print("or:", loud(True) or loud(False))

user_input = ""
name = user_input or "Anonymous"
print(name)
$ python logical.py
default

second
  evaluated False
and: False
  evaluated True
or: True
Anonymous

The second line is blank: "" and "never" returned its falsy left operand, the empty string itself, which print shows as nothing. In the loud calls only the left side was evaluated, because the result was already decided. That short-circuit is what makes name = user_input or "Anonymous" a safe default.

Membership

in tests containment; what “contains” means depends on the type.

print("pha" in "alphabet", "z" not in "alphabet")
print(3 in [1, 2, 3], 5 not in {1, 2, 3})

user = {"name": "Alice", "age": 30}
print("name" in user, "Alice" in user, "Alice" in user.values())
print((1, 2) in [(1, 2), (3, 4)])
$ python membership.py
True True
True True
True False True
True

On strings, in is a substring test. On dicts it checks keys, so "Alice" in user is False — use .values() to search values. in on a set or dict is a hash lookup (fast); on a list it scans every element.

Bitwise operators and flags

Bitwise operators work on the binary digits of integers — the classic use is packing flags into one int.

a, b = 0b1010, 0b1100
print(a & b, a | b, a ^ b, ~a, a << 1, a >> 1)
print(bin(a & b), bin(a | b), bin(a ^ b))

READ, WRITE, EXECUTE = 0b001, 0b010, 0b100
perms = READ | WRITE
print(bin(perms), bool(perms & READ), bool(perms & EXECUTE))
perms &= ~WRITE
print(bin(perms))
$ python bitwise.py
8 14 6 -11 20 5
0b1000 0b1110 0b110
0b11 True False
0b1

~a is -11, not some large positive number: Python ints have unlimited size, and ~x is defined as -x - 1. perms & ~WRITE clears exactly one bit and leaves the others alone.

The walrus operator :=

:= assigns and returns a value in the same expression.

import io
import re

stream = io.StringIO("abcdefghij")
while chunk := stream.read(4):
    print(repr(chunk))

data = ["user@example.com", "invalid", "admin@company.org"]
pattern = re.compile(r"\w+@(\w+)\.\w+")
domains = [m.group(1) for s in data if (m := pattern.match(s))]
print(domains)

if (n := len(data)) > 2:
    print(f"{n} items")
$ python walrus.py
'abcd'
'efgh'
'ij'
['example', 'company']
3 items

The loop reads, tests and keeps each chunk in one line, and stops when read returns the empty (falsy) string. In the comprehension the regex runs once per item; without := you’d call match once to filter and again to extract. The parentheses matter: n := len(data) > 2 would bind n to True.

Conditional expression

a if condition else b is an expression, so it fits where a statement can’t.

for score in (95, 85, 40):
    grade = "A" if score >= 90 else "B" if score >= 80 else "F"
    print(score, grade, "pass" if score >= 50 else "fail")
$ python ternary.py
95 A pass
85 B pass
40 F fail

Chained conditionals read left to right, but more than one else ... if quickly hurts readability — use if/elif or a lookup instead.

Operator overloading

Operators call special methods, so your own classes can support them.

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    def __mul__(self, k):
        return Vector(self.x * k, self.y * k)
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v = Vector(1, 2) + Vector(3, 4)
print(v, v * 2, v == Vector(4, 6))
try:
    print(2 * v)
except TypeError as e:
    print("TypeError:", e)
$ python vector.py
Vector(4, 6) Vector(8, 12) True
TypeError: unsupported operand type(s) for *: 'int' and 'Vector'

v * 2 calls v.__mul__(2), but 2 * v asks int first, which doesn’t know about vectors, and then looks for Vector.__rmul__ — which isn’t defined. Adding __rmul__ = __mul__ fixes it.

Practice

1. Predict, then check: 7 // 2, -7 // 2, 7 % -2 and divmod(-7, 2).

Solution
print(7 // 2, -7 // 2, 7 % -2, divmod(-7, 2))
$ python floor_div.py
3 -4 -1 (-4, 1)

Floor division rounds toward negative infinity and % takes the divisor’s sign; divmod returns both at once.

2. Write is_between(x, lo, hi) using a chained comparison, and test it on 5, 0 and 10 with bounds 0 and 10.

Solution
def is_between(x, lo, hi):
    return lo <= x <= hi

print([is_between(x, 0, 10) for x in (5, 0, 10, 11)])
$ python between.py
[True, True, True, False]

3. Add an EXECUTE permission to perms = READ with |=, then check both READ and WRITE in one expression.

Solution
READ, WRITE, EXECUTE = 0b001, 0b010, 0b100
perms = READ
perms |= EXECUTE
print(bin(perms))
needed = READ | WRITE
print(perms & needed == needed, perms & READ == READ)
$ python flags.py
0b101
False True

perms & needed == needed is true only if every bit in needed is set; here WRITE is missing. (== binds looser than & in Python, so no parentheses are required — but adding them helps readers.)

4. Add __rmul__ to Vector so that 2 * v works.

Solution
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __mul__(self, k):
        return Vector(self.x * k, self.y * k)
    __rmul__ = __mul__
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v = Vector(4, 6)
print(2 * v, v * 3)
$ python rmul.py
Vector(8, 12) Vector(12, 18)

When int.__mul__ returns NotImplemented, Python tries the right operand’s __rmul__.

Frequently Asked Questions

What is the walrus operator?
The walrus operator := assigns a value to a name and also returns that value, so you can compute something, test it and reuse it in one expression. It is most useful in while loop conditions and comprehensions, and was added in Python 3.8.
What's the difference between / and // in Python?
/ is true division and always returns a float, even for 6 / 3. // is floor division: it rounds down toward negative infinity, so 7 // 2 is 3 but -7 // 2 is -4.
How does Python's 'not in' operator work?
x not in c is True when x is not found in c. It works on strings (substring test), lists, tuples, sets and dicts, where it checks the keys rather than the values.
What do 'and' and 'or' return in Python?
They return one of their operands, not necessarily True or False. a or b returns a if a is truthy, otherwise b; a and b returns a if a is falsy, otherwise b. That is why name or 'Anonymous' works as a default.
When should I use 'is' instead of '=='?
Use is only to test identity, which in practice means comparisons with singletons such as None, True and False. Use == to compare values. Two equal lists, strings or numbers may or may not be the same object, so is gives unreliable answers for them.