3.1 Operator Categories
Python's operators fall into 8 categories — each covered in its own sub-section below, with the operator symbols and a simple example.
- 3.1.1 Arithmetic Operators
- 3.1.2 Relational Operators
- 3.1.3 Boolean Operators
- 3.1.4 Non-Boolean Operators
- 3.1.5 Bitwise Operators
- 3.1.6 Assignment Operators
- 3.1.7 Ternary Operators
- 3.1.8 Special Operators — Identity and Membership
3.1.1 Arithmetic Operators
| Operator | Meaning |
|---|---|
+ |
Addition |
- |
Subtraction |
* |
Multiplication |
/ |
Division — always returns a float, even for exact division |
// |
Floor division — divides then rounds down (towards -∞) |
% |
Modulus — remainder left over after // division |
** |
Exponentiation |
Run the cell below and compare each output to the operator used.
a, b = 7, 2
print("a + b =", a + b) # addition
print("a - b =", a - b) # subtraction
print("a * b =", a * b) # multiplication
print("a / b =", a / b) # division -> always a float
print("a // b =", a // b) # floor division -> rounds down to whole number
print("a % b =", a % b) # modulus -> remainder of a / b
print("a ** b =", a ** b) # exponent -> a raised to the power b
a + b = 9 a - b = 5 a * b = 14 a / b = 3.5 a // b = 3 a % b = 1 a ** b = 49
Common mistake — // and % round towards -∞, not towards zero, so results with a
negative operand can be surprising:
print(-7 // 2, -7 % 2) # -4 1 -> NOT -3, -1 (which is what "towards zero" would give)
-4 1
Beyond numbers, + and * also work on sequences — str, list, tuple — but they
mean something different there: + concatenates (joins two sequences into a new one)
and * repeats (builds a new sequence by repeating the operand). These are separate
from the numeric arithmetic above — -, /, //, % aren't defined for sequences at all.
Going deeper — this isn't hardcoded into +/*; str/list/tuple implement it
themselves via __add__/__mul__ — operator overloading, covered in 13.2.
print("ab" + "cd") # abcd -> concatenation, not addition
print([0] * 5) # [0, 0, 0, 0, 0] -> repetition, not multiplication
print("-" * 40) # a 40-character separator line, handy for printed output
abcd [0, 0, 0, 0, 0] ----------------------------------------
In practice — CLI output formatting and building SQL. "-" * 60 for a readable
separator line in command-line tool output, or ", ".join(["?"] * len(values)) to
build a batched SQL IN (?, ?, ?) clause with the right number of placeholders (18.4),
both lean directly on sequence repetition/concatenation rather than a loop.
3.1.2 Relational Operators
| Operator | Meaning |
|---|---|
== |
Equal to |
!= |
Not equal to |
> |
Greater than |
< |
Less than |
>= |
Greater than or equal to |
<= |
Less than or equal to |
These compare value and always produce a bool — don't confuse this with is, which
compares object identity instead (3.1.8).
a, b = 5, 3
print("a == b:", a == b) # equal?
print("a != b:", a != b) # not equal?
print("a > b :", a > b) # greater than?
print("a < b :", a < b) # less than?
print("a >= b:", a >= b) # greater than or equal?
print("a <= b:", a <= b) # less than or equal?
a == b: False a != b: True a > b : True a < b : False a >= b: True a <= b: False
Comparisons can also be chained: a < b < c is not evaluated left-to-right as
(a < b) < c — Python treats it as a < b and b < c, except each operand (b here) is
evaluated only once. Plenty of languages don't have this at all; it reads naturally and
avoids repeating a subexpression.
print(1 < 5 < 10) # True -> equivalent to (1 < 5) and (5 < 10)
print(1 < 5 < 3) # False -> 5 < 3 is False, so the whole chain is False
True False
3.1.3 Boolean Operators
| Operator | Meaning |
|---|---|
and |
True only if both operands are True |
or |
True if at least one operand is True |
not |
Flips a True/False value |
With bool operands the result is always a bool — 3.1.4 covers what happens when the
operands aren't bool at all.
print("True and False:", True and False) # both must be True -> False
print("True or False :", True or False) # either can be True -> True
print("not True :", not True) # flips it -> False
True and False: False True or False : True not True : False
The same and / or operators also work on non-bool operands. Instead of a bool,
they return one of the actual operands — whichever one decided the outcome — based on
truthiness:
Note — "truthy" means a value that acts like True in a boolean context (e.g.
inside an if); "falsy" means it acts like False. Every object is one or the other — the
full list of falsy values is in 4.1.
andevaluates left to right and stops at the first falsy value (or returns the last value if every operand is truthy).orevaluates left to right and stops at the first truthy value (or returns the last value if every operand is falsy).
This is called short-circuit evaluation — the second operand is never even evaluated
once the result is already decided. It's commonly used to supply a default value, e.g.
name = user_input or "Guest".
print("3 and 5:", 3 and 5) # both truthy -> `and` returns the LAST one -> 5
print("0 and 5:", 0 and 5) # 0 is falsy -> `and` stops early, returns it -> 0
print("3 or 5 :", 3 or 5) # 3 is truthy -> `or` stops early, returns it -> 3
print("0 or 5 :", 0 or 5) # 0 is falsy -> `or` moves on, returns the next -> 5
3 and 5: 5 0 and 5: 0 3 or 5 : 3 0 or 5 : 5
3.1.5 Bitwise Operators
| Operator | Meaning |
|---|---|
& |
AND — bit set only where both operands have a 1 |
\| |
OR — bit set where either operand has a 1 |
^ |
XOR — bit set where the operands differ |
~ |
NOT — flips every bit |
<< |
Left shift — shifts bits left (equivalent to multiplying by 2ⁿ) |
>> |
Right shift — shifts bits right (equivalent to floor-dividing by 2ⁿ) |
These operate directly on an integer's binary representation, and are common in flags, bitmasks and low-level/performance-sensitive code.
a, b = 5, 3 # in binary: a = 101, b = 011
print("a & b :", a & b) # AND -> bit set only where BOTH have 1 -> 001 = 1
print("a | b :", a | b) # OR -> bit set where EITHER has 1 -> 111 = 7
print("a ^ b :", a ^ b) # XOR -> bit set where they DIFFER -> 110 = 6
print("~a :", ~a) # NOT -> flips all bits -> -6
print("a << 1:", a << 1) # shift bits left (x2) -> 10
print("a >> 1:", a >> 1) # shift bits right (/2, rounded down) -> 2
a & b : 1 a | b : 7 a ^ b : 6 ~a : -6 a << 1: 10 a >> 1: 2
A common real use of bitwise operators is flags: pack several boolean options into a
single integer using distinct powers of two, combine them with |, and test for one with
&. Separately — ~x flips every bit, which for signed integers works out to
~x == -(x + 1); that's why ~5 above is -6.
READ = 1 # 0b001
WRITE = 2 # 0b010
EXEC = 4 # 0b100
perms = READ | WRITE
print(perms) # 3 -> READ and WRITE both set
print(bool(perms & READ)) # True -> READ bit is set
print(bool(perms & EXEC)) # False -> EXEC bit is not set
3 True False
In practice — real permission systems. Unix file permissions (rwx bits), Django
REST Framework's permission flags, and feature-flag systems in backend config all pack
several independent boolean options into one integer using exactly this bitmask
pattern — combining with |, checking with &, precisely as shown above.
3.1.6 Assignment Operators
= binds a name to an object. The augmented forms update and reassign in one step:
| Operator | Equivalent to |
|---|---|
+= |
x = x + y |
-= |
x = x - y |
*= |
x = x * y |
/= |
x = x / y |
//= |
x = x // y |
%= |
x = x % y |
**= |
x = x ** y |
&=, \|=, ^=, <<=, >>= |
same idea, for the bitwise operators (3.1.5) |
= also supports chained assignment (a = b = c = 5) and multiple
assignment/unpacking (a, b = 1, 2).
a = b = c = 5
print(a, b, c) # chained assignment -> all three point to the same object
x, y = 1, 2
print(x, y) # unpacking -> x gets 1, y gets 2
n = 5
n += 3 # same as: n = n + 3
print(n) # 8
5 5 5 1 2 8
For mutable types like list, += mutates the same object in place; plain x = x + [...]
always builds a brand-new object. Compare the two below:
x = [1, 2]
y = x
x += [3] # in-place: mutates the same object
print("y after += :", y) # [1, 2, 3] -> y sees the change too
x = [1, 2]
y = x
x = x + [3] # NOT in-place: builds a new list
print("y after x=x+[3]:", y) # [1, 2] -> y is unaffected
y after += : [1, 2, 3] y after x=x+[3]: [1, 2]
Going deeper — += isn't magic: Python first looks for an __iadd__ method on the left
operand. A mutable type like list defines it to mutate in place and return self — which
is why y saw the change above. If __iadd__ isn't defined (as for int, str, tuple),
Python falls back to __add__ and rebinds the name to a brand-new object instead. 13.2
covers operator overloading in full.
:= — the walrus operator — is also an assignment operator, but unlike
= it's usable inside an expression: it assigns a value to a name and produces that same
value as the expression's result. Handy when you'd otherwise compute something once just to
test it, then use it again right after.
data = [1, 2, 3]
if (n := len(data)) > 2:
print(n) # 3 -> n was assigned inside the condition and is still usable here
3
In practice — reading a stream in fixed-size chunks. while (chunk := f.read(8192)):
is the standard production idiom for reading a large file, a socket, or a file-upload
body in bounded chunks instead of all at once — the walrus operator lets the read and
the "did we get anything?" check happen in one line instead of two.
3.1.7 Ternary Operators
A conditional expression — not a statement — that picks one of two values inline:
value_if_true if condition else value_if_false. Because it's an expression (it evaluates
to a value), it can be used anywhere a value is expected — assignment, a function argument,
inside an f-string — a full if/else statement cannot. Python has no separate ?:
symbol like C/Java/JavaScript; it spells the same idea out with keywords instead.
x = 7
result = "even" if x % 2 == 0 else "odd"
print(result) # odd
x = 0 # ternaries can be chained for more than 2 outcomes
print("positive" if x > 0 else "negative" if x < 0 else "zero") # zero
odd zero
In practice — resolving a config value with a fallback. timeout = config.get("timeout")
if config else DEFAULT_TIMEOUT-style expressions are a common real pattern in settings/
config-loading code, picking an override when one is present and falling back to a
sane default otherwise, all in one line.
3.1.8 Special Operators — Identity and Membership
Identity (is, is not) compares object identity — whether two names point to the
exact same object in memory (id()) — not whether their values are equal. Two
equal-value objects are usually not the same object (2.4 covers when CPython does, or
doesn't, reuse an existing object).
Membership (in, not in) checks whether a value exists inside a sequence or
collection — list, tuple, str, set, or the keys of a dict.
x = [1, 2, 3]
y = [1, 2, 3]
print("x == y:", x == y) # True -> same value
print("x is y:", x is y) # False -> NOT the same object
print("id(x) :", id(x)) # each object has its own id
print("id(y) :", id(y)) # a different id, even though the value is equal
print("3 in x :", 3 in x) # True -> 3 is present in x
print("5 not in x:", 5 not in x) # True -> 5 is absent from x
x == y: True x is y: False id(x) : 140458367031808 id(y) : 140458367005760 3 in x : True 5 not in x: True
In practice — an access-control check. if role in ADMIN_ROLES: guarding an
admin-only action is a real, extremely common use of membership testing — and per
3.1.8's own note above, using a set for ADMIN_ROLES instead of a list keeps that
check fast even as the list of privileged roles grows.
Going deeper — membership testing (in) is O(n) for a list or tuple (Python scans
element by element) but O(1) on average for a set or dict (hash-based lookup) — chapter
5 covers why.
The precedence table just below also lists three unary operators — operators that take
a single operand, written immediately before it: -x (negation), +x (unary plus — the
value unchanged, defined mostly for symmetry with -x), and ~x (bitwise NOT — flips every
bit, 3.1.5).
x = 5
print(-x) # -5 -> negation
print(+x) # 5 -> unary plus, value unchanged
print(~x) # -6 -> bitwise NOT: flips every bit
-5 5 -6
3.2 Precedence and Associativity
Precedence decides which operator binds first when an expression mixes several of them. When in doubt, parenthesize — this table is a reference, not a memorization target. From highest to lowest:
| # | Operators | Category |
|---|---|---|
| 1 | () |
Parentheses (grouping) |
| 2 | ** |
Exponentiation |
| 3 | +x, -x, ~x |
Unary plus / minus / bitwise not |
| 4 | *, /, //, % |
Multiplication, division, floor division, modulus |
| 5 | +, - |
Addition, subtraction |
| 6 | <<, >> |
Bitwise shifts |
| 7 | & |
Bitwise AND |
| 8 | ^ |
Bitwise XOR |
| 9 | \| |
Bitwise OR |
| 10 | ==, !=, >, <, >=, <=, is, is not, in, not in |
Comparisons |
| 11 | not |
Boolean NOT |
| 12 | and |
Boolean AND |
| 13 | or |
Boolean OR |
| 14 | x if c else y |
Ternary |
| 15 | =, +=, -=, ... |
Assignment |
print(2 + 3 * 4) # 14 -> * (row 4) binds tighter than + (row 5)
print((2 + 3) * 4) # 20 -> parentheses (row 1) always override the default order
14 20
Common mistake — unary minus binds looser than ** (row 3 vs row 2 in the table
above), so -2 ** 2 is -(2 ** 2), not (-2) ** 2:
print(-2 ** 2) # -4 -> ** binds first: -(2 ** 2), NOT (-2) ** 2 which would be 4
print((-2) ** 2) # 4 -> parenthesize to force the other order
-4 4
Associativity decides the order when operators of the same precedence appear
together. Most operators are left-to-right; ** and assignment are the exceptions —
they are right-to-left (which is what makes chained assignment in 3.1.6 work: a = b = 5
assigns to b first, then assigns that same object to a).
print(2 ** 3 ** 2) # 512 -> evaluated right-to-left as 2 ** (3 ** 2) = 2 ** 9, not (2**3)**2 = 64
512