Skip to content

Operators#

Operators are how you combine values, compare them, and control evaluation order. In interviews, operator behavior shows up constantly in edge cases — integer division, chained comparisons, short-circuit logic, bitwise tricks, and the subtle difference between value comparison and identity.

How to use this page

Read arithmetic and comparison first, then logical/identity, then bitwise. Revisit Interview traps before mocks. Continue with Control Flow and Strings.

At a glance
Track Python Basics
Sections 9 major topics
Outline Use the right-hand TOC to jump

Topics: How Python evaluates expressions · Arithmetic operators · Comparison operators · Logical operators · Membership and identity operators · Bitwise operators · Assignment operators · Operator precedence (highest → lowest) · … (+1 more)

  1. How Python evaluates expressions
  2. Arithmetic operators
  3. Comparison operators
  4. Logical operators
  5. Membership and identity operators
  6. Bitwise operators
  7. Assignment operators
  8. Operator precedence (highest → lowest)
  9. Type-specific operator behavior

How Python evaluates expressions#

Before individual operators, understand the evaluation model:

  1. Operands are objects — operators invoke special methods (__add__, __eq__, etc.) on those objects.
  2. Most operators create or return objects — even and/or return an operand, not necessarily bool.
  3. Short-circuit operators (and, or) skip evaluating the right side when the result is already determined.
  4. Precedence decides grouping when parentheses are absent — when unsure, add parentheses.
class Box:
    def __init__(self, value):
        self.value = value
    def __add__(self, other):
        return Box(self.value + other.value)

Box(2) + Box(3)   # Box(5) — + dispatches to __add__

You rarely define __add__ in interviews, but knowing operators are syntax sugar for method calls explains why "a" + 1 raises TypeError while 1 + 1.5 works (int defines __add__ accepting float).


Arithmetic operators#

Operator Name Example Result
+ Addition 3 + 2 5
- Subtraction 5 - 2 3
* Multiplication 4 * 3 12
/ True division 7 / 2 3.5 (always float)
// Floor division 7 // 2 3
% Modulo (remainder) 7 % 2 1
** Exponentiation 2 ** 10 1024
+x, -x Unary plus/minus -(-5) 5

True division vs floor division vs modulo#

Python 3 made / always return a float. This differs from C, Java, and Python 2:

Expression Python 3 C/Java (int/int)
7 / 2 3.5 3
7 // 2 3 3
-7 / 2 -3.5 -3
-7 // 2 -4 -3

Floor division rounds toward negative infinity, not toward zero:

import math

7 // 2      # 3
-7 // 2     # -4  (math.floor(-3.5) == -4)
7 // -2     # -4
-7 // -2    # 3

# Toward-zero truncation (like C) — use int() on float division
int(-7 / 2)  # -3

The divmod identity (always holds in Python 3):

a, b = 17, 5
q, r = divmod(a, b)
assert q == a // b
assert r == a % b
assert a == b * q + r
# divmod(17, 5) → (3, 2)

Modulo sign follows the divisor (b), not the dividend:

-7 % 3    # 2   because -7 = 3*(-3) + 2
7 % -3    # -2  because 7 = (-3)*(-3) + (-2)

Code & explanation — clock arithmetic

1
2
3
4
5
6
def mod(n: int, m: int) -> int:
    """Normalize n into [0, m) — useful for circular buffers."""
    return (n % m + m) % m

mod(-1, 24)   # 23 — "one hour before midnight"
mod(25, 24)   # 1
Explanation

  1. Double modulo handles negative dividends cleanly.
  2. Use this in rotating array / cyclic string problems instead of manual if n < 0.

Exponentiation nuances#

2 ** 10       # 1024
2 ** 0.5      # 1.414... — float result
pow(2, 10)    # same as 2 ** 10
pow(2, 10, 1000)  # 24 — modular exponent (2^10 % 1000), efficient for large exponents

# Right-associative: 2 ** 3 ** 2 == 2 ** (3 ** 2) == 2 ** 9
2 ** 3 ** 2   # 512, not 64

math.pow always returns float; ** preserves int when both operands are int and result is integral.

Augmented assignment (+=, -=, …)#

Operator Example
+= x += 1
-= x -= 1
*= x *= 2
/= x /= 2
//= x //= 2
%= x %= 2
**= x **= 2
&=, \|=, ^=, <<=, >>= Bitwise compound

Critical nuance: augmented assignment may mutate in place or rebind depending on type:

# Immutable — creates new object, rebinds name
s = "hi"
id_before = id(s)
s += "!"
id(s) != id_before   # True — new str object

# Mutable list — often mutates in place
nums = [1, 2]
id_before = id(nums)
nums += [3]          # calls __iadd__ → extend-like
id(nums) == id_before  # True — same list object

# BUT: rebind vs mutate depends on object
t = (1, 2)
# t += (3,)  # creates NEW tuple, rebinds t — tuples are immutable
Type += behavior
int, float, str, tuple New object, rebind
list, dict, set In-place mutation when __iadd__/update applies
list with + Always new list: a = a + [x] differs from a += [x] for some custom classes
a = [1, 2]
b = a
a = a + [3]     # new list; b still [1, 2]
a = [1, 2]
b = a
a += [3]        # mutates; b is [1, 2, 3]

Comparison operators#

Operator Meaning Calls
== Equal value __eq__
!= Not equal __ne__
<, >, <=, >= Ordering __lt__, __gt__, etc.
3 == 3.0          # True — numeric equality across types
"a" < "b"         # True — lexicographic (Unicode code point order)
[1, 2] < [1, 3]   # True — lexicographic on sequences
(1, 2) < (1, 3)   # True
None == None      # True — but prefer `is None` for identity

Chained comparisons#

Python evaluates chained comparisons as and of pairwise comparisons — without re-evaluating middle operands:

lo <= x <= hi           # lo <= x and x <= hi
1 < a < b < 10

# Middle operand evaluated once — matters with side effects
# x is evaluated once in: lo <= x <= hi

This is idiomatic for range checks and avoids repeating variables.

Comparing different types#

Python 3 does not compare arbitrary incompatible types (unlike Python 2):

# "3" < 3   # TypeError in Python 3
[] < ()     # False — compares by type name order (implementation detail, avoid relying on)

For interviews: ensure comparable types before sorting or using <.

Float and NaN comparisons#

float("nan") == float("nan")   # False — NaN is not equal to itself
float("nan") != float("nan")   # True
import math
math.isnan(x)                  # correct NaN test

Never use == to compare floats for equality in production — use math.isclose:

math.isclose(0.1 + 0.2, 0.3)   # True
math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)

Identity vs equality (with operators)#

Operator Compares Example
== Values [1,2] == [1,2] → True
is Identity (same object) [1,2] is [1,2] → False

See Python Language Fundamentals for the full is/== guide.


Logical operators#

Operator Behavior
and Short-circuit: first falsy operand, or last if all truthy
or Short-circuit: first truthy operand, or last if all falsy
not Always returns True or False
0 or 42           # 42 — not True/False!
"" or "default"   # "default"
1 and 2 and 3     # 3
0 and expensive() # expensive() never called
[] or [1]         # [1]

Return value is the operand itself, not coerced to bool — this enables patterns:

# Default fallback
name = user_input or "anonymous"

# Conditional execution
callback and callback()

# Guard chaining
if user and user.is_active and user.has_permission("read"):
    ...

De Morgan's laws (refactoring conditions)#

not (a and b)  # equivalent to (not a) or (not b)
not (a or b)   # equivalent to (not a) and (not b)

Useful when simplifying negated compound conditions.

Boolean operators vs bitwise operators#

True & False   # False — bitwise AND on bool (both evaluated!)
True and False # False — short-circuit

# Never use & / | for logical conditions on booleans — no short-circuit

&, |, ^ on bools evaluate both sides — use and/or for logic.


Membership and identity operators#

Operator Tests Typical complexity
in / not in Value in container See table below
is / is not Same object O(1)

in complexity by container type#

Container x in container
list, tuple O(n) linear scan
dict O(1) avg — tests keys, not values
set O(1) avg
str O(n·m) substring search (CPython uses optimized algorithms)
3 in [1, 2, 3]        # True
"py" in "python"      # True — substring
"a" in {"a": 1}       # True — key membership
1 in {"a": 1}         # False — 1 is not a key

Three ways to test dict membership#

d = {"name": "Alice", "age": 30}

# Key exists?
"name" in d
d.keys()     # same for `in`

# Value exists? (O(n))
30 in d.values()

# Key-value pair?
("name", "Alice") in d.items()

For value lookup, invert the map or maintain a secondary index if needed frequently.


Bitwise operators#

Essential for Bit Manipulation:

Operator Meaning Example
& AND 5 & 31 (101 & 011 = 001)
\| OR 5 \| 37
^ XOR 5 ^ 36
~ NOT (invert bits) ~0-1 (two's complement)
<< Left shift 1 << 38
>> Right shift 8 >> 22

Negative numbers use two's complement — bitwise on negatives is error-prone in interviews. Stick to non-negative inputs unless the problem requires otherwise.

Core bitwise identities#

x & 1           # LSB — 1 if odd, 0 if even
x >> 1          # floor divide by 2
x << 1          # multiply by 2
x ^ x           # 0
x ^ 0           # x
x & (x - 1)     # clear lowest set bit
x & -x          # isolate lowest set bit

Interview patterns (multiple approaches)#

Check odd/even:

# Approach 1: modulo
n % 2 == 1

# Approach 2: bitwise (preferred in bit problems)
n & 1

Count set bits:

def popcount(n: int) -> int:
    count = 0
    while n:
        n &= n - 1
        count += 1
    return count

# Or: bin(n).count("1") — readable, slightly slower

Power of two:

def is_power_of_two(n: int) -> bool:
    return n > 0 and (n & (n - 1)) == 0

Swap without temp (XOR — know conceptually, rarely use in Python):

a, b = b, a   # Pythonic — use this in interviews
# XOR swap: a ^= b; b ^= a; a ^= b  — don't use in real Python code

Subset enumeration via bitmask:

def all_subsets(nums: list[int]) -> list[list[int]]:
    n = len(nums)
    result = []
    for mask in range(1 << n):
        result.append([nums[i] for i in range(n) if mask & (1 << i)])
    return result

Set algebra: operators vs methods#

a = {1, 2, 3}
b = {3, 4, 5}

a | b     # union: {1, 2, 3, 4, 5}
a & b     # intersection: {3}
a - b     # difference: {1, 2}
a ^ b     # symmetric difference: {1, 2, 4, 5}

# Method form — accepts any iterable
a.union([6, 7])
a.update([6])       # in-place union
In-place method Operator equivalent
s \|= t s.update(t)
s &= t s.intersection_update(t)
s -= t s.difference_update(t)
s ^= t s.symmetric_difference_update(t)

Assignment operators#

Form Meaning
= Bind name to object
:= (walrus, 3.8+) Assign and return value in expression

Walrus operator — when and when not#

# Good: avoid double call / double lookup
while (line := input()) != "quit":
    process(line)

if (match := pattern.search(text)):
    print(match.group())

# Good: reuse expensive computation in comprehension filter
results = [y for x in data if (y := transform(x)) > threshold]

# Avoid: walrus where simple assignment before block is clearer
if (n := len(nums)) > 0:   # fine
    ...

Operator precedence (highest → lowest)#

Level Operators Notes
1 ( ), [ ], { } Grouping, subscription, display
2 ** Right-associative: 2**3**2 = 2**(3**2)
3 +x, -x, ~x Unary
4 *, /, //, %
5 +, - Binary
6 <<, >>
7 &
8 ^
9 \|
10 Comparisons (==, <, in, is, …) Chained
11 not
12 and
13 or
14 Conditional (x if c else y) Ternary
15 Lambda
16 =, +=, …, := Assignment

When in doubt, use parentheses.

not a and b       # (not a) and b
not (a and b)     # different
2 ** 3 ** 2       # 512
(2 ** 3) ** 2     # 64

Type-specific operator behavior#

Strings and sequences#

"hello" + " world"    # concatenation — O(n+m) new string
"ha" * 3              # "hahaha" — repetition
[1, 2] + [3]          # [1, 2, 3] — new list
[0] * 5               # five references to same 0 (immutable, OK)
[[0]] * 3             # BUG if inner mutable — three refs to same list

Interview trap — nested multiplication

row = [0] * 3
grid = [row] * 3
grid[0][0] = 1
print(grid)   # [[1,0,0], [1,0,0], [1,0,0]]

# Fix 1: list comprehension
grid = [[0] * 3 for _ in range(3)]

# Fix 2: deep init
grid = [[0 for _ in range(3)] for _ in range(3)]

Lists: + vs += vs extend#

a = [1, 2]
b = a + [3]       # new list [1,2,3]; a unchanged
a += [3]          # mutates a in place
a.extend([3])     # same effect as += for lists

Interview traps (quick reference)#

Trap What goes wrong Safe approach
/ vs // Expecting integer from / Use // for floor division
Negative // and % Differs from C/Java Test edge cases; use divmod
is for values Wrong identity check Use == for content
[x] * n nested Shared inner references List comprehension
Short-circuit side effects Right side skipped Don't rely on side effects in and/or
& vs and Both sides evaluated with & Use and/or for logic
Float == Precision errors math.isclose
NaN comparison Always False for == math.isnan
in on dict Tests keys only Use .values() or invert map
2 ** 3 ** 2 Right-associative Parenthesize explicitly

Mental model checklist#

  1. What is the difference between / and // for negative operands?
  2. How does divmod(a, b) relate to // and %?
  3. What does and/or return — always bool?
  4. Why is x is None preferred over x == None?
  5. What is the O(.) complexity of x in my_list vs x in my_set?
  6. How do you test whether a number is a power of two with one bitwise expression?

What's next#

Topic Page
Branching and loops Control Flow
String operations Strings
Collections Data Structures
Bit manipulation patterns Bit Manipulation