Lesson 4 of 25

Operators

Arithmetic, and What Division Really Returns

Python's seven arithmetic operators are the ones you would expect plus two that C-style languages spell differently. Addition, subtraction and multiplication behave exactly as they look. The interesting three are /, // and **.

/ is true division and always returns a float, even when the result is a whole number. 10 / 5 is 2.0, not 2. Students coming from C or Java, or from old Python 2 tutorials, expect integer input to give integer output and are surprised when an index calculation produces 3.0 and then fails with "list indices must be integers". If you want a whole number, ask for one with //.

// is floor division: it divides and then rounds down to the nearest whole number. ** is exponentiation, so 2 ** 10 is 1024 — note that Python uses this rather than a pow function or a ^ symbol. ^ in Python is bitwise XOR, so writing 2 ^ 3 expecting 8 silently gives you 1 instead. That is one of the quieter bugs on this page precisely because it produces a number rather than an error.

Example
a, b = 17, 5

print(a + b)    # 22
print(a - b)    # 12
print(a * b)    # 85
print(a / b)    # 3.4    true division  -> always float
print(a // b)   # 3      floor division -> whole number
print(a % b)    # 2      remainder
print(a ** 2)   # 289    exponentiation

print(10 / 5)         # 2.0  — a float, even though it divides evenly
print(type(10 / 5))   # <class 'float'>
print(10 // 5)        # 2    — int in, int out

# ^ is NOT power in Python
print(2 ** 3)   # 8   correct
print(2 ^ 3)    # 1   bitwise XOR — no error, just wrong

# divmod gives both results in one call
print(divmod(17, 5))   # (3, 2)
  • + - * — as expected; + also joins strings and lists
  • / — true division, result is always a float
  • // — floor division, rounds down toward negative infinity
  • % — remainder; the classic even/odd test is n % 2 == 0
  • ** — power; 2 ** 0.5 is a square root
  • divmod(a, b) — returns the quotient and remainder together as a tuple

Floor Division and Modulo Go Strange with Negatives

With positive numbers, // behaves like the integer division you learned in school: 7 // 2 is 3. With negatives it does something most other languages do not. -7 // 2 is -4, not -3. The word "floor" is literal — Python rounds down the number line, and -4 is below -3.5 while -3 is above it.

The remainder follows from that choice. Python guarantees that (a // b) * b + (a % b) == a, so once floor division rounds down, the remainder must take the sign of the divisor to make the identity hold. That gives -7 % 2 == 1, where C and Java would give -1.

This is genuinely useful more often than it is annoying. Because % with a positive divisor always returns a non-negative result, wrap-around arithmetic just works: rotating an index around a list, mapping any hour number onto a 12-hour clock, or cycling through colours. Where it bites is when you port a formula from a C or Java textbook and the negative cases quietly disagree. If you need truncation toward zero instead, use int(a / b), and be aware that route goes through a float and loses precision on very large integers.

Example
print(7 // 2)     #  3
print(-7 // 2)    # -4   floors DOWN, not toward zero
print(7 // -2)    # -4

print(7 % 2)      #  1
print(-7 % 2)     #  1   sign follows the divisor
print(7 % -2)     # -1

# The identity Python preserves
a, b = -7, 2
print((a // b) * b + (a % b) == a)   # True

# Useful consequence: safe wrap-around, no negative index worries
colours = ["red", "green", "blue"]
for step in range(-3, 4):
    print(step, colours[step % 3])

# If you truly want truncation toward zero
print(int(-7 / 2))    # -3
Notes
  • % on floats works but inherits floating-point error, so 0.3 % 0.1 does not give a clean 0. Keep modulo for integers unless you have thought hard about the tolerance.

Augmented Assignment and the List Surprise

x += 5 is shorthand for x = x + 5, and every arithmetic operator has a matching form: -=, *=, /=, //=, %=, **=. For numbers the two spellings are interchangeable, and the short one is preferred because it names the variable once — halving the chance of updating the wrong one in a long expression.

For mutable objects they are not interchangeable, and this catches people out. x = x + [4] builds a brand-new list and re-points the name at it. x += [4] modifies the existing list in place, exactly like calling .extend(). If another variable is pointing at that same list, the first version leaves it alone and the second version changes what it sees.

Note also that Python has no ++ or --. Writing x++ is a SyntaxError; ++x is legal but does nothing useful, since it parses as "positive of positive of x". Use x += 1.

Example
score = 10
score += 5      # 15
score -= 3      # 12
score *= 2      # 24
score //= 5     # 4
print(score)    # 4

# Same spelling, different behaviour on lists
a = [1, 2, 3]
b = a
a = a + [4]          # NEW list; b keeps the old one
print(a, b)          # [1, 2, 3, 4] [1, 2, 3]

a = [1, 2, 3]
b = a
a += [4]             # modifies the list in place
print(a, b)          # [1, 2, 3, 4] [1, 2, 3, 4]

# No increment operator
# score++            # SyntaxError
score += 1
Notes
  • += on a string is safe because strings are immutable — it always creates a new string. Building a long string that way inside a loop is still slow, though; collect the pieces in a list and call "".join(pieces) once at the end.

Comparison Operators and Chaining

The six comparison operators — ==, !=, >, <, >=, <= — each produce a boolean. The one to write carefully is ==. A single = is assignment, and in an if statement Python will reject it with a SyntaxError rather than silently assigning, which is a small kindness the language does for you.

Python lets you chain comparisons the way mathematics does: 0 <= marks <= 100 means exactly what it looks like, and reads better than marks >= 0 and marks <= 100. It is not just cosmetic — the middle expression is evaluated only once, which matters if it is a function call.

Comparison across incompatible types is where Python differs from JavaScript. "10" == 10 is False, with no conversion attempted, and "10" > 5 raises a TypeError instead of guessing. Strings compare to each other by Unicode code point, which means "Zebra" < "apple" is True — every uppercase letter sorts before every lowercase one. For human-facing sorting, compare .lower() versions.

Example
marks = 78

print(marks == 78)     # True
print(marks != 78)     # False
print(0 <= marks <= 100)   # True — chained, reads like maths

# Chaining evaluates the middle only once
def get_marks():
    print("reading marks...")
    return 78

if 0 <= get_marks() <= 100:      # "reading marks..." prints ONCE
    print("valid")

# No implicit conversion across types
print("10" == 10)      # False
# print("10" > 5)      # TypeError

# String comparison is by character code
print("apple" < "banana")    # True
print("Zebra" < "apple")     # True  — capitals sort first
print("Zebra".lower() < "apple".lower())   # False — what a human expects
  • == equal in value, != not equal
  • < > <= >= — ordering; raise TypeError across unrelated types
  • Chaining: a < b < c is a < b and b < c, with b computed once
  • = assigns; == compares. Python rejects = inside an if condition
  • Lists and tuples compare element by element, left to right

Logical Operators Return Values, Not Just True and False

Python spells its logical operators and, or and not rather than &&, || and !. They short-circuit: and stops at the first falsy operand and or stops at the first truthy one, without evaluating the rest. That is not an optimisation detail, it is a tool. Writing if user is not None and user.is_active: is safe precisely because the second half never runs when the first half fails.

The part that surprises people is what these operators actually return. They do not return True or False — they return one of the operands. 0 or "guest" evaluates to the string "guest", and "Ravi" and "Kumar" evaluates to "Kumar". You get a boolean in an if only because the if converts whatever it receives. This is why name = entered_name or "Guest" is such a common idiom for defaults.

The classic beginner bug lives here too. if choice == "a" or "b": looks like it asks whether choice is one of two letters. It does not. Python reads it as (choice == "a") or ("b"), and since the non-empty string "b" is always truthy, the whole condition is always True. Write if choice in ("a", "b"): instead — shorter, correct, and it scales to ten options.

Example
print(True and False)   # False
print(True or False)    # True
print(not True)         # False

# They return an operand, not a boolean
print(0 or "guest")         # guest
print("Ravi" and "Kumar")   # Kumar
print([] or "empty")        # empty

# Default-value idiom
entered = ""
name = entered or "Guest"
print(name)     # Guest

# Short-circuiting protects the second test
user = None
if user is not None and user["active"]:    # safe: never indexes None
    print("active user")

# THE classic bug
choice = "z"
if choice == "a" or "b":
    print("matched")     # prints! "b" is truthy, so this is always True

if choice in ("a", "b"):
    print("matched")     # correct: prints nothing
Notes
  • and and or bind more loosely than comparison, so a < b and c < d needs no brackets. not binds more loosely than comparison too, which is why not x == y means not (x == y) — though x != y says it more clearly.

Membership, Identity and Precedence

in tests membership and works on every container: strings, lists, tuples, sets and dictionaries. On a dictionary it checks the keys, not the values, which is a distinction worth fixing in your memory now because it is a frequent source of confusion. On a set it is dramatically faster than on a list — a set lookup is roughly constant time while a list has to scan every element — which is why converting a long list to a set before repeated membership tests is a standard speed-up.

is looks similar and is not. It asks whether two names point at the same object rather than whether they hold equal values. Reserve it for None, True and False; use == everywhere else.

Precedence follows normal mathematics with two entries worth memorising. ** binds tighter than unary minus, so -2 ** 2 is -4, not 4. And ** groups right to left, so 2 ** 3 ** 2 is 2 ** 9, which is 512 rather than 64. When an expression mixes three or more operators, brackets cost nothing and remove all doubt for the next person reading it.

Example
print("th" in "python")            # True
print(3 in [1, 2, 3])              # True
print("name" in {"name": "Asha"})  # True  — checks KEYS
print("Asha" in {"name": "Asha"})  # False — not values
print("Asha" in {"name": "Asha"}.values())   # True

# Sets are far faster for repeated lookups
valid_ids = {"A101", "A102", "A103"}
print("A102" in valid_ids)         # True

# Precedence traps
print(-2 ** 2)        # -4   ** binds tighter than the minus sign
print((-2) ** 2)      #  4
print(2 ** 3 ** 2)    # 512  right-to-left: 2 ** (3 ** 2)
print((2 ** 3) ** 2)  #  64

print(2 + 3 * 4)      # 14   * before +
print((2 + 3) * 4)    # 20
  • in / not in — membership; checks dictionary keys, not values
  • is / is not — identity; use only with None, True, False
  • Precedence, tightest first: **, unary -, * / // %, + -, comparisons, not, and, or
  • ** is right-associative; every other arithmetic operator is left-associative
Notes
  • Python also has bitwise operators — & and, | or, ^ xor, ~ not, << and >> shifts. You will rarely need them in application code, but sets reuse &, | and ^ for intersection, union and symmetric difference, so the symbols are worth recognising.
Ask AI