Operators & Expressions
Topic 2 of 8, with 4 concept checks. Logical, bitwise, arithmetic, and overloaded operators
Predict how Python evaluates an expression
Evaluation order
Read an expression in Python's actual precedence order, distinguish logical from bitwise behavior, and decide which special method participates when an operator is overloaded.
Core lesson 01
and/or are logical and short-circuit, returning an operand. &/| are bitwise, always evaluate both sides.
`and`/`or` are control-flow operators: they evaluate left to right and stop as soon as the result is determined, returning the actual operand (not necessarily True/False). `&`/`|` are bitwise operators — they always evaluate both sides and operate bit-by-bit on integers (they're also overloaded by set for union/intersection).
def noisy(label, val):
print("eval", label)
return val
noisy("a", False) and noisy("b", True) # only prints 'eval a'
noisy("a", False) & noisy("b", True) # prints both -- always evaluatesWhat to remember
What's the difference between and/or and &/| for booleans?
Common footguns
- Using `&`/`|` for boolean logic in an `if` — works for plain booleans but breaks intent and precedence expectations.
- Forgetting `&`/`|` bind tighter than comparisons: `a & b == c` isn't what it looks like — need `(a & b) == c`.
a() and b() if a() is False -> b() NEVER called (short-circuit) a() or b() if a() is True -> b() NEVER called (short-circuit)
Core lesson 02
True — numeric types compare by value, not by exact type.
Python's numeric tower (bool, int, float, complex) defines cross-type equality and arithmetic. `==` between numbers compares mathematical value, so 3 == 3.0 is True even though `type(3) is int` and `type(3.0) is float`. This is different from `is`, which would say False since they're distinct objects.
print(3 == 3.0) # True
print(3 is 3.0) # False
print(type(3) == type(3.0)) # FalseWhat to remember
What does 3 == 3.0 evaluate to, and why?
Common footguns
- Assuming equal values always means equal types — always true for numbers, but not a general Python rule.
Core lesson 03
Implement __add__(self, other) — Python calls it automatically whenever + is used between instances.
Operator overloading in Python works through 'dunder' (double-underscore) methods. `a + b` is really syntactic sugar for `a.__add__(b)` (falling back to `b.__radd__(a)` if the first returns NotImplemented). This is how built-in types support `+` too — int, str, and list all define their own `__add__`.
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 __repr__(self):
return f"Vector({self.x}, {self.y})"
print(Vector(1,2) + Vector(3,4)) # Vector(4, 6)What to remember
How would you make a custom class support the + operator?
Common footguns
- Forgetting to return NotImplemented (not raise) when the other operand's type isn't supported, which breaks fallback to __radd__.
- Mutating self inside __add__ instead of returning a new object — breaks the usual 'a+b doesn't change a' expectation.
Core lesson 04
-10 % 3 = 2, because Python's % always matches the sign of the divisor.
Python defines `%` so that `a % b` always has the same sign as `b` (when b != 0), consistent with floor division: `a == (a // b) * b + (a % b)`. In C-family languages, `%` instead takes the sign of the dividend, so -10 % 3 would be -1 there.
print(-10 % 3) # 2
print(10 % -3) # -2
print(-10 // 3) # -4
print(-4*3 + 2) # -10 (checks out)What to remember
What's the result of -10 % 3 in Python?
Common footguns
- Porting modulo-based logic (e.g. hashing, wraparound indices) from C/Java and getting different results for negative numbers.
Python lab
Browser Python lab
Runtime · idle
Python loads on your first run. Your code stays in this browser.
Best practices
- Use and/or for boolean logic; reserve &/| for bitwise ops or pandas/numpy boolean arrays.
- Avoid unclear chained comparisons unless they genuinely improve readability.
- Prefer `x += 1` over `x = x + 1` for simple in-place-style updates.
- Use parentheses to make operator precedence explicit, even when not strictly required.
Apply the concept in Interview practice
Power of TwoeasyLeetCode #231 · O(1) time/space
n is a power of two if n>0 and (n & (n-1)) == 0 — a power of two has exactly one bit set.
Open problemDivide Two IntegersmediumLeetCode #29 · O(log n) time
Simulate division via repeated doubling/bit-shifting of the divisor instead of * or /; handle sign and 32-bit overflow separately.
Open problemMini-Max SumeasyHackerRank · O(n log n) time
Sort the array; the min sum excludes the largest element, the max sum excludes the smallest.
Open problemSingle NumbereasyLeetCode #136 · O(n) time, O(1) space
XOR every number together — a value XORed with itself is 0, and XOR is commutative/associative, so all paired values cancel out, leaving only the single one.
Open problemSum of Two IntegersmediumLeetCode #371 · O(1) time (bounded by int width)
Simulate addition with bitwise XOR for the sum-without-carry and AND+shift for the carry, looping until there's no carry left.
Open problemConcept checks
What's the difference between and/or and &/| for booleans?
Hint
One short-circuits and may skip evaluating the right side.
The other always evaluates both sides — it's a bitwise operator underneath.
Answer
and/or are logical and short-circuit, returning an operand. &/| are bitwise, always evaluate both sides.
`and`/`or` are control-flow operators: they evaluate left to right and stop as soon as the result is determined, returning the actual operand (not necessarily True/False). `&`/`|` are bitwise operators — they always evaluate both sides and operate bit-by-bit on integers (they're also overloaded by set for union/intersection).
def noisy(label, val):
print("eval", label)
return val
noisy("a", False) and noisy("b", True) # only prints 'eval a'
noisy("a", False) & noisy("b", True) # prints both -- always evaluatesa() and b() if a() is False -> b() NEVER called (short-circuit) a() or b() if a() is True -> b() NEVER called (short-circuit)
Watch out
- Using `&`/`|` for boolean logic in an `if` — works for plain booleans but breaks intent and precedence expectations.
- Forgetting `&`/`|` bind tighter than comparisons: `a & b == c` isn't what it looks like — need `(a & b) == c`.
What does 3 == 3.0 evaluate to, and why?
Hint
Value equality doesn't care what class the object is.
Python compares the numeric value, not type(x) == type(y).
Answer
True — numeric types compare by value, not by exact type.
Python's numeric tower (bool, int, float, complex) defines cross-type equality and arithmetic. `==` between numbers compares mathematical value, so 3 == 3.0 is True even though `type(3) is int` and `type(3.0) is float`. This is different from `is`, which would say False since they're distinct objects.
print(3 == 3.0) # True
print(3 is 3.0) # False
print(type(3) == type(3.0)) # FalseWatch out
- Assuming equal values always means equal types — always true for numbers, but not a general Python rule.
How would you make a custom class support the + operator?
Hint
There's a special 'dunder' method for addition.
It's named __add__.
Answer
Implement __add__(self, other) — Python calls it automatically whenever + is used between instances.
Operator overloading in Python works through 'dunder' (double-underscore) methods. `a + b` is really syntactic sugar for `a.__add__(b)` (falling back to `b.__radd__(a)` if the first returns NotImplemented). This is how built-in types support `+` too — int, str, and list all define their own `__add__`.
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 __repr__(self):
return f"Vector({self.x}, {self.y})"
print(Vector(1,2) + Vector(3,4)) # Vector(4, 6)Watch out
- Forgetting to return NotImplemented (not raise) when the other operand's type isn't supported, which breaks fallback to __radd__.
- Mutating self inside __add__ instead of returning a new object — breaks the usual 'a+b doesn't change a' expectation.
What's the result of -10 % 3 in Python?
Hint
Python's modulo result takes the sign of the divisor, not the dividend.
This is different from C-style languages.
Answer
-10 % 3 = 2, because Python's % always matches the sign of the divisor.
Python defines `%` so that `a % b` always has the same sign as `b` (when b != 0), consistent with floor division: `a == (a // b) * b + (a % b)`. In C-family languages, `%` instead takes the sign of the dividend, so -10 % 3 would be -1 there.
print(-10 % 3) # 2
print(10 % -3) # -2
print(-10 // 3) # -4
print(-4*3 + 2) # -10 (checks out)Watch out
- Porting modulo-based logic (e.g. hashing, wraparound indices) from C/Java and getting different results for negative numbers.