Operators & Expressions
1 · The lesson
readOperators are the symbols and keywords that do something with values. You've seen + and = already. This lesson covers the operators you will use every day: arithmetic, comparison, logical, membership and assignment operators. It also covers the precedence rules, which decide which operator runs first when a line has several.
Most operators behave as they do in maths. This lesson spends most of its time on the places where they don't.
1. Arithmetic Operators
print(7 + 3) # 10 print(7 - 3) # 4 print(7 * 3) # 21 print(7 / 3) # 2.3333333333333335 (true division always gives a float) print(7 // 3) # 2 (floor division drops the remainder) print(7 % 3) # 1 (modulo gives the remainder) print(7 ** 3) # 343 (7 to the power of 3)
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 //.
% (remainder) is useful for "every Nth" checks. Want to do something every 5 iterations? Check 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.
print(9 % 3 == 0) # True: 9 is a multiple of 3 print(10 % 3 == 0) # False: 10 / 3 leaves a remainder of 1 print(7 % 2 == 0) # False: 7 is odd print(370 % 360) # 10: turning 370 degrees ends at the same place as 10 degrees
For floating-point problems like 0.1 + 0.2 != 0.3, go 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 will use comparisons in conditions and loops all the time.
Chained comparisons. Many languages make you write x > 0 && x < 10. Python lets you write the comparison the way it is written in maths:
x = 5 print(0 < x < 10) # True (it reads like maths) print(0 < x < 10 < 100) # True (a chain can be as long as you like)
This is shorter and easier to read, so use it.
3. == vs is: Equality vs Identity
== asks "do these have the same value?" is asks "are these the same object in memory?" These are different questions, and they can have different answers.
a = [1, 2, 3] b = [1, 2, 3] c = a print(a == b) # True (same contents) print(a is b) # False (two different list objects) print(a is c) # True (the same object, as in the Variables lesson)
Rule of thumb: use == for almost everything. Use is only to compare with the three singletons (objects that exist only once): None, True and False.
The next examples use if, which the Conditionals lesson teaches. For now, read if result is None: as "run the indented lines only when result is None is True".
if result is None: # correct ... if result == None: # works, but is not good Python style, and code checkers 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 important part 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, because `False and ...` is already False result = False and expensive_check() print(result) # False, with no print from the function
You can use this to avoid errors:
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. Python programmers use this pattern very often.
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. Learn 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)
An empty tuple () is falsy too. Everything else is truthy. This means you rarely write if len(items) > 0:. You write the shorter 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 (checks for a substring) print(3 in [1, 2, 3]) # True (checks for an item in a list) print("name" in {"name": "Ada"})# True (checks the keys of a dict, not the values) print(99 not in [1, 2, 3]) # True
One line does the work of a whole loop.
7. Assignment Operators
The short forms do the same thing as the long version:
x = 10 x += 3 # same as x = x + 3, so x becomes 13 x -= 5 # 8 x *= 2 # 16 x //= 3 # 5 x %= 2 # 1 x **= 4 # 1
One detail to know. It follows from the mutability section in the Variables lesson:
# Lists: += changes the same list in place lst = [1, 2] lst += [3, 4] print(lst) # [1, 2, 3, 4] (the same list, now longer) # Strings can't be changed (they are immutable), so += builds a new string s = "hi" s += " there" print(s) # "hi there" (but this is a new string object)
For one or two concatenations the difference doesn't matter. For thousands of string appends in a loop, build a list and call "".join(parts) at the end. This is much faster.
8. Operator Precedence
When a line has several operators, which one runs first? Operators with higher precedence run first:
| 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 the rough order.
2. Use parentheses wherever a reader might need to stop and think. That reader may be you, a few months later.
# 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')
Extra parentheses cost nothing, and they make the code easier to read.
Common Mistakes
=vs==.=assigns,==compares.if x = 5:is aSyntaxErrorin Python. (In C, the same mistake runs without an error.) Inside a condition you always want==.- Treating chained comparisons like other languages. In Python,
1 < x < 10does what it says: it checks thatxis between 1 and 10. In C, the same expression is read as(1 < x) < 10, which is always true, and in Java it does not compile. So if you translate code from those languages, don't copy their longer form out of habit. - Forgetting short-circuit. If the right-hand side has side effects (a function call, an API hit), changing the order in an
and/orcan change what the program does, without any error.expensive() and cheap()runsexpensive()first every time, butcheap() 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 usually work, butis Noneis the standard way. Linters will warn about the first.
🎯 Your Turn: Password Strength Checker
Write a short program that checks one password and prints whether it is strong. In this exercise, a password is strong when all four of these are true:
- it has at least 12 characters
- it contains an uppercase letter
- it contains a lowercase letter
- it contains a symbol (anything that isn't a letter or a digit)
Skeleton:
pw = "Hello123!world" # TODO 1: long_enough should be True if pw has at least 12 characters # TODO 2: has_upper should be True if pw contains an uppercase letter # TODO 3: has_lower should be True if pw contains a lowercase letter # TODO 4: has_symbol should be True if pw contains a symbol # TODO 5: combine the four with `and`, then print each check and the result
Run it. Then change pw to "hello123", "Hello123" and "PASSWORD!", and run it again each time.
Hint 1: Each check is one comparison
len(pw) gives the number of characters, and len(pw) >= 12 is already a boolean. You don't need anything else for TODO 1.
Hint 2: Finding letters and symbols without a loop
pw.lower() returns a copy of the string with every letter in lowercase. If that copy is different from the original (pw.lower() != pw), then pw had at least one uppercase letter. pw.upper() != pw finds a lowercase letter in the same way. pw.isalnum() is True when every character is a letter or a digit, so not pw.isalnum() means there is at least one symbol.
Show full solution
pw = "Hello123!world" long_enough = len(pw) >= 12 has_upper = pw.lower() != pw # lowering changed something, so pw had an uppercase letter has_lower = pw.upper() != pw has_symbol = not pw.isalnum() strong = long_enough and has_upper and has_lower and has_symbol print("Long enough:", long_enough) print("Uppercase:", has_upper) print("Lowercase:", has_lower) print("Symbol:", has_symbol) print("Strong:", strong)
Output:
Long enough: True Uppercase: True Lowercase: True Symbol: True Strong: True
With pw = "Hello123", the program prints Strong: False: the password has only 8 characters and no symbol. Each check is one comparison or one not, and and combines them into one answer. In the Conditionals lesson you will use booleans like these with if, for example to print "weak" or "strong".
What You Learned
/always returns a float,//is floor division, and%is the remainder, which you can use for "every Nth" checks.==compares values, andiscompares identity. Useisonly forNone,True,False.and/orshort-circuit. This helps you avoid errors onNoneand skip slow 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 valid Python and easy to read. - Operators have a precedence order. Add parentheses whenever they make the code clearer.
Next: Comments and style, for writing code that other people can read.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.