Operators & Expressions
1 · The lesson
readOperators are the verbs of Python. You've seen + and = already — this lesson covers the full set you'll use daily: arithmetic, comparison, logic, membership, and assignment, plus the precedence rules that decide who wins when several operators meet on the same line.
The behaviour mostly mirrors maths. The interesting parts are where it doesn't.
1. Arithmetic Operators
print(7 + 3) # 10 print(7 - 3) # 4 print(7 * 3) # 21 print(7 / 3) # 2.3333333333333335 — true division, always float print(7 // 3) # 2 — floor division, drops the remainder print(7 % 3) # 1 — modulo, the remainder print(7 ** 3) # 343 — exponentiation
Two of these surprise newcomers:
/ always returns a float, even when the result is whole. 6 / 2 is 3.0, not 3. If you need an integer, use //.
% is "every Nth" in disguise. Want to do something every 5 iterations? Check if i % 5 == 0. Want to know if a number is even? n % 2 == 0. Want a value to wrap around a circle of 360 degrees? angle % 360.
for i in range(1, 11): if i % 3 == 0: print(f"{i} is a multiple of 3")
For floating-point quirks like 0.1 + 0.2 != 0.3, jump back to Numbers in Depth. The short version: never compare floats with ==; use math.isclose().
2. Comparison Operators
print(5 == 5) # True — equal print(5 != 4) # True — not equal print(5 < 10) # True print(5 > 10) # False print(5 <= 5) # True print(5 >= 6) # False
Every comparison returns a bool. You'll chain them into conditions and loops constantly.
The Python gift — chained comparisons. Most languages force you to write x > 0 && x < 10. Python lets you write what mathematicians actually write:
x = 5 print(0 < x < 10) # True — reads like maths print(0 < x < 10 < 100) # True — chain as long as you like
This is genuinely useful, not a gimmick. Use it.
3. == vs is — Equality vs Identity
== asks "do these have the same value?" is asks "are these the same object in memory?" Different questions, different answers.
a = [1, 2, 3] b = [1, 2, 3] c = a print(a == b) # True — same contents print(a is b) # False — different list objects print(a is c) # True — same object (we saw this in the Variables lesson)
Rule of thumb: use == for almost everything. Use is only for comparing against the three singletons — None, True, False:
if result is None: # correct ... if result == None: # works, but unidiomatic — linters will warn you ...
setup added so this can run · defines result
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) result = _AutoMock('result')
4. Logical Operators and Short-Circuit Evaluation
print(True and False) # False print(True or False) # True print(not True) # False
The interesting bit is short-circuit evaluation. Python evaluates left-to-right and stops as soon as the answer is decided:
andstops at the first falsy valueorstops at the first truthy value
def expensive_check(): print("ran expensive_check") return True # expensive_check never runs — `False and ...` is already False result = False and expensive_check() print(result) # False, with no print from the function
This is how you guard against errors safely:
user = None # Without short-circuit, the second condition would crash on None.name if user is not None and user.name == "Ada": print("Hello, Ada")
If user is not None is False, Python never evaluates user.name, so the AttributeError never happens. Idiomatic Python relies on this constantly.
5. Truthiness — What Counts as False
Python evaluates any value in a boolean context (like if or while). The falsy values are a short list — memorise them:
bool(False) # False bool(None) # False bool(0) # False bool(0.0) # False bool("") # False — empty string bool([]) # False — empty list bool({}) # False — empty dict bool(set()) # False — empty set
Everything else is truthy. This means you rarely write if len(items) > 0: — you write the cleaner version:
items = [1, 2, 3] if items: # truthy because non-empty print("got items") name = "" if not name: # falsy because empty print("name is missing")
6. Membership: in and not in
print("py" in "python") # True — substring check print(3 in [1, 2, 3]) # True — element in list print("name" in {"name": "Ada"})# True — key in dict (not value) print(99 not in [1, 2, 3]) # True
One line replaces a whole loop. Use it.
7. Assignment Operators
The shorthand forms are sugar over the long version:
x = 10 x += 3 # same as x = x + 3 → 13 x -= 5 # 8 x *= 2 # 16 x //= 3 # 5 x %= 2 # 1 x **= 4 # 1
One subtlety — and it's a direct consequence of the mutability discussion in the Variables lesson:
# Lists: += mutates in place lst = [1, 2] lst += [3, 4] print(lst) # [1, 2, 3, 4] — same list, just extended # Strings: += can't mutate (strings are immutable) — it builds a new object s = "hi" s += " there" print(s) # "hi there" — but a new string was created
For one or two concatenations the difference doesn't matter. For thousands of string appends in a loop, build a list and "".join(parts) at the end — far faster.
8. Operator Precedence
When multiple operators meet, who runs first? Highest precedence binds tightest:
| Precedence | Operators | Notes |
|---|---|---|
| Highest | ** | Exponentiation |
+x, -x, ~x | Unary plus/minus | |
*, /, //, % | Multiplicative | |
+, - | Additive | |
<, <=, >, >=, ==, !=, in, is | Comparison | |
not | Boolean NOT | |
and | Boolean AND | |
or | Boolean OR | |
| Lowest | =, +=, etc. | Assignment |
Two ways to use this table:
1. Read it once so you know roughly where things sit.
2. Use parentheses anywhere a code reviewer might pause. Your future self counts as a reviewer.
# Technically correct, briefly confusing: if x > 0 and y > 0 or z == 0: ... # Same logic, zero confusion: if (x > 0 and y > 0) or z == 0: ...
setup added so this can run · defines z, x, y
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) z = _AutoMock('z') x = _AutoMock('x') y = _AutoMock('y')
Parentheses are free. Cognitive load isn't.
Common Mistakes
=vs==.=assigns,==compares.if x = 5:is aSyntaxErrorin Python (a small mercy compared to C). Inside a condition you always want==.- Treating chained comparisons like other languages.
1 < x < 10is the good news — it does what you'd hope. The bad news only hits if you're translating from C or Java where1 < x < 10parses as(1 < x) < 10and means something nonsensical. In Python it's correct. Use it. - Forgetting short-circuit. If the right-hand side has side effects (a function call, an API hit), reordering an
and/orcan silently change behaviour.expensive() and cheap()runsexpensive()first every time;cheap() and expensive()only runsexpensive()whencheap()is truthy. - Comparing floats with
==.0.1 + 0.2 == 0.3isFalse. Usemath.isclose(). See Numbers in Depth. if value == None:instead ofif value is None:. Both work, only one is idiomatic. Linters will flag the first.
🎯 Your Turn — Password Strength Checker
Write a function password_strength(pw) that returns one of three strings — "weak", "medium", or "strong" — based on these rules:
- weak: shorter than 8 characters
- medium: at least 8 characters, and contains at least two of {lowercase letter, uppercase letter, digit, symbol}
- strong: at least 12 characters and contains all four categories
A symbol is anything that isn't a letter or a digit.
Skeleton:
def password_strength(pw): # TODO 1: short-circuit the weak case first # TODO 2: figure out which character categories are present # TODO 3: decide medium vs strong based on count and length ... print(password_strength("hi")) # weak print(password_strength("hello123")) # medium print(password_strength("Hello123")) # medium print(password_strength("Hello123!world")) # strong
Hint 1 — Detecting character categories
Strings have helpful methods:c.islower(), c.isupper(), c.isdigit(), c.isalnum(). Loop through the password once and set four boolean flags. A symbol is anything where c.isalnum() is False.
Hint 2 — Counting how many categories matched
Booleans are integers in disguise —True is 1, False is 0. So sum([has_lower, has_upper, has_digit, has_symbol]) gives you a count from 0 to 4. That's the cleanest way to express "at least two of these".
Show full solution
def password_strength(pw): if len(pw) < 8: return "weak" has_lower = any(c.islower() for c in pw) has_upper = any(c.isupper() for c in pw) has_digit = any(c.isdigit() for c in pw) has_symbol = any(not c.isalnum() for c in pw) categories = sum([has_lower, has_upper, has_digit, has_symbol]) if len(pw) >= 12 and categories == 4: return "strong" if categories >= 2: return "medium" return "weak" tests = ["hi", "hello123", "Hello123", "Hello123!world", "PASSWORD"] for pw in tests: print(f"{pw:<20} → {password_strength(pw)}")
Output:
hi → weak hello123 → medium Hello123 → medium Hello123!world → strong PASSWORD → weak
The any() calls with generator expressions read like English: "is any character a lowercase letter?" Summing booleans is a small but very Pythonic trick — you'll see it again the moment you start counting things.
What You Learned
/always returns a float;//is floor division;%is the remainder — and it's how you write "every Nth".==compares values;iscompares identity. Useisonly forNone,True,False.and/orshort-circuit — useful for guarding againstNoneand skipping expensive calls.- The falsy values are a short list:
False,None,0,0.0,"",[],{},set(). Everything else is truthy. in/not inwork on strings, lists, dicts (keys), and sets.- Chained comparisons (
0 < x < 10) are real Python and read beautifully. - Operator precedence exists; parentheses are free — use them whenever clarity wins.
Next: Lists — your first deep dive into a mutable collection, building on everything you've seen so far.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code actually did, not just whether a test passed.