Python · lesson 5 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Python 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 |
| boolean | not, then and, then or |
| conditional, assignment expression | x 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__.