PythonMastery
beginner 14 min read · lesson 4 of 19 in Python Fundamentals

Operators & Expressions

1 · The lesson

read

Operators 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

python
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.

python
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

python
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:

python
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.

python
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:

python
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

python
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:

  • and stops at the first falsy value
  • or stops at the first truthy value
python
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:

python
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:

python
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:

python
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

python
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:

python
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:

python
# 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:

PrecedenceOperatorsNotes
Highest**Exponentiation
+x, -x, ~xUnary plus/minus
*, /, //, %Multiplicative
+, -Additive
<, <=, >, >=, ==, !=, in, isComparison
notBoolean NOT
andBoolean AND
orBoolean 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.

python
# 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 a SyntaxError in Python (a small mercy compared to C). Inside a condition you always want ==.
  • Treating chained comparisons like other languages. 1 < x < 10 is the good news — it does what you'd hope. The bad news only hits if you're translating from C or Java where 1 < x < 10 parses as (1 < x) < 10 and 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/or can silently change behaviour. expensive() and cheap() runs expensive() first every time; cheap() and expensive() only runs expensive() when cheap() is truthy.
  • Comparing floats with ==. 0.1 + 0.2 == 0.3 is False. Use math.isclose(). See Numbers in Depth.
  • if value == None: instead of if 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:

python
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
python
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:

python
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; is compares identity. Use is only for None, True, False.
  • and/or short-circuit — useful for guarding against None and skipping expensive calls.
  • The falsy values are a short list: False, None, 0, 0.0, "", [], {}, set(). Everything else is truthy.
  • in / not in work 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.in

Short exercises that run in your browser and tell you what your code actually did, not just whether a test passed.