Python Operators
Operators are the symbols and keywords that tell Python to perform an action on one or more values: add two numbers, compare them, combine boolean conditions, or check whether an item exists in a collection. Every operator you type is shorthand for a specific instruction the interpreter carries out, and many of them are actually syntactic sugar for method calls defined on the objects themselves. Understanding how operators really work — not just memorizing what each symbol does — helps you avoid classic bugs like confusing = with ==, and explains why is and == sometimes disagree about values that look identical.
Overview: How Operators Work
An operator combines one or more operands (the values being acted on) into an expression, which Python evaluates down to a single value. Python groups operators into several families:
- Arithmetic —
+ - * / // % **— numeric math. - Assignment —
= += -= *= /=etc. — store or update a value in a variable. - Comparison —
== != < > <= >=— compare two values and produce abool. - Logical —
and or not— combine boolean expressions. - Identity —
is,is not— check whether two names refer to the exact same object in memory. - Membership —
in,not in— check whether a value exists inside a container. - Bitwise —
& | ^ ~ << >>— operate on the individual bits of integers.
Under the hood, most operators are not special-cased by the interpreter — they are calls to dunder (double-underscore) methods defined on the operand’s type. When you write a + b, Python actually evaluates type(a).__add__(a, b). If that returns NotImplemented (because the left type doesn’t know how to add a right-hand type), Python falls back to type(b).__radd__(b, a) before giving up and raising a TypeError. This is called operator overloading, and it’s why + means numeric addition for int, concatenation for str and list, and can be given a completely custom meaning on your own classes by defining __add__.
Comparison and logical operators also rely on object protocols: == calls __eq__, < calls __lt__, and so on. and, or, and not work with truthiness rather than strict booleans — every object in Python is considered truthy unless its type defines it as falsy (0, 0.0, "", [], {}, None, and False itself are falsy). Crucially, and/or use short-circuit evaluation: they stop as soon as the result is determined and return one of the original operands, not necessarily True/False. For example 0 or "fallback" evaluates to "fallback", and "" and expensive_call() never calls expensive_call() at all.
Finally, expressions with multiple operators follow a fixed precedence and associativity table, similar to arithmetic in math class but extended to cover every operator Python supports. Parentheses always override precedence, and using them liberally makes intent explicit even when they aren’t strictly required.
Syntax
The general shape of an operator expression is:
operand1 operator operand2
(unary operators like not and - take a single operand written before it, e.g. -x or not done). The table below summarizes the most common operators, roughly from highest to lowest precedence:
| Category | Operators | Example | Result |
|---|---|---|---|
| Exponent | ** |
2 ** 3 |
8 |
| Unary | +x -x ~x |
-5 |
-5 |
| Multiplicative | * / // % |
7 // 2 |
3 |
| Additive | + - |
3 - 1 |
2 |
| Comparison | == != < > <= >= |
3 < 5 |
True |
| Identity / Membership | is, is not, in, not in |
3 in [1, 2, 3] |
True |
| Logical NOT | not |
not True |
False |
| Logical AND | and |
True and False |
False |
| Logical OR | or |
False or True |
True |
| Assignment | = += -= *= /= //= %= **= |
x += 1 |
updates x |
Examples
Example 1: Arithmetic Operators in a Shopping Cart
This example uses arithmetic operators to compute a subtotal, apply a discount, and figure out how many full boxes are needed for shipping.
price = 49.99
quantity = 3
discount_rate = 0.15
subtotal = price * quantity
discount = subtotal * discount_rate
total = subtotal - discount
print(f"Subtotal: ${subtotal:.2f}")
print(f"Discount: ${discount:.2f}")
print(f"Total: ${total:.2f}")
items_per_box = 4
boxes_needed = quantity // items_per_box
leftover = quantity % items_per_box
print(f"Full boxes needed: {boxes_needed}, leftover items: {leftover}")
power_demo = 2 ** 10
print(f"2 to the 10th power is {power_demo}")
Output:
Subtotal: $149.97
Discount: $22.50
Total: $127.47
Full boxes needed: 0, leftover items: 3
2 to the 10th power is 1024
* and - compute the subtotal and total as expected. // is floor division — it divides and rounds down to the nearest whole number, so 3 items with 4 per box produce 0 full boxes. % is the modulo operator, returning the remainder (3 leftover items). ** raises a number to a power.
Example 2: Comparison and Logical Operators for Eligibility Checks
Here, comparison operators produce booleans, and logical operators combine them — including Python’s short-circuit behavior and chained comparisons.
def check_eligibility(age: int, has_membership: bool, credit_score: int) -> bool:
is_adult = age >= 18
good_credit = credit_score >= 650
eligible = is_adult and (has_membership or good_credit)
print(f"age={age}, has_membership={has_membership}, credit_score={credit_score} -> eligible={eligible}")
return eligible
applicants = [
(17, True, 700),
(25, False, 600),
(30, False, 720),
(40, True, 500),
]
results = [check_eligibility(age, member, score) for age, member, score in applicants]
print(f"Approved count: {results.count(True)}")
a, b, c = 5, 10, 15
print(1 < a < b < c)
print(a == 5 and not (b > c))
Output:
age=17, has_membership=True, credit_score=700 -> eligible=False
age=25, has_membership=False, credit_score=600 -> eligible=False
age=30, has_membership=False, credit_score=720 -> eligible=True
age=40, has_membership=True, credit_score=500 -> eligible=True
Approved count: 2
True
True
The 17-year-old is rejected because is_adult is False, and thanks to short-circuiting, Python never even evaluates has_membership or good_credit for that operand — and already knows the whole expression is False. Notice 1 < a < b < c: Python supports chained comparisons, which are evaluated as 1 < a and a < b and b < c, not as a left-to-right chain the way some other languages would (mis)interpret it.
Example 3: Identity, Equality, Membership, and Operator Overloading
This example contrasts == (value equality) with is (identity), checks membership with in, and shows a custom class that overloads + and == by defining __add__ and __eq__.
class Vector2D:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __add__(self, other: "Vector2D") -> "Vector2D":
return Vector2D(self.x + other.x, self.y + other.y)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector2D):
return NotImplemented
return self.x == other.x and self.y == other.y
def __repr__(self) -> str:
return f"Vector2D({self.x}, {self.y})"
v1 = Vector2D(1, 2)
v2 = Vector2D(3, 4)
v3 = v1 + v2
print(v3)
v4 = Vector2D(4, 6)
print(v3 == v4)
print(v3 is v4)
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits)
print("grape" not in fruits)
list_a = [1, 2, 3]
list_b = [1, 2, 3]
print(list_a == list_b)
print(list_a is list_b)
print(list_a is not list_b)
Output:
Vector2D(4, 6)
True
False
True
True
True
False
True
v1 + v2 works because Vector2D defines __add__; without it, Python would raise TypeError: unsupported operand type(s) for +. v3 == v4 is True because __eq__ compares the x and y values, but v3 is v4 is False because they are two distinct objects in memory. The same distinction appears with list_a and list_b: they hold equal values (== is True) but are different list objects (is is False).
Under the Hood: Precedence and Augmented Assignment
When an expression mixes several operators, Python resolves it using the precedence table shown above, then evaluates left to right (with ** being right-associative). Walk through this expression step by step:
result = 2 + 3 * 4 ** 2 - (6 / 2)
print(result)
x = 10
x += 5
x -= 2
x *= 3
x //= 4
print(x)
Output:
47.0
9
Step by step, Python evaluates 4 ** 2 first (exponent has the highest precedence) to get 16; then 3 * 16 to get 48 (multiplication before addition); the parenthesized (6 / 2) becomes 3.0; finally 2 + 48 - 3.0 is evaluated left to right to give 47.0 — the result is a float because / (true division) always produces one, and mixing a float into addition/subtraction promotes the whole result to float.
The second block shows augmented assignment: x += 5 is roughly equivalent to x = x + 5, tracing through 10 -> 15 -> 13 -> 39 -> 9. For immutable types like int, += always creates a new object and rebinds the name. For mutable types like lists, though, += calls __iadd__ if it exists, which mutates the object in place instead of creating a new one — this matters a lot if another variable references the same list, and is a common source of subtle bugs (see Common Mistakes below).
Common Mistakes
Mistake 1: Using = Instead of ==
= assigns a value; == compares two values. Writing an assignment where a comparison is expected is such a common typo that Python refuses to even compile it as a condition:
age = 20
if age = 20:
print("Same age")
This raises SyntaxError: invalid syntax because = is a statement, not an expression, and cannot appear inside an if condition. The fix is to use the comparison operator:
age = 20
if age == 20:
print("Same age")
Output:
Same age
Mistake 2: Confusing / and //
/ always performs true division and returns a float, even when the numbers divide evenly. Using it when you actually want whole units (like minutes and leftover seconds) produces a technically correct but practically unhelpful number:
total_seconds = 150
minutes = total_seconds / 60
print(f"That is {minutes} minutes")
Output:
That is 2.5 minutes
That’s accurate, but if you actually want separate whole minutes and remaining seconds, use // for the whole minutes and % for the remainder instead:
total_seconds = 150
minutes = total_seconds // 60
seconds = total_seconds % 60
print(f"That is {minutes} minutes and {seconds} seconds")
Output:
That is 2 minutes and 30 seconds
Mistake 3: Using is for Value Comparison
Because is checks object identity rather than equality, using it to compare things like numbers, strings, or lists is unreliable. CPython caches small integers (-5 to 256) and some string literals as an internal optimization, so x is y can accidentally return True for small numbers in quick experiments, then behave completely differently for larger numbers or values built at runtime — as seen with list_a is list_b in Example 3, where two equal-valued lists were still different objects. Reserve is for identity checks that are supposed to be about identity, most commonly x is None, and use == everywhere else.
Best Practices
- Use
==/!=to compare values; reserveis/is notfor identity checks, especiallyis None(PEP 8 explicitly recommends this over== None). - Prefer
//and%when you need whole-number division and a remainder; use/when you actually want a float result. - Use augmented assignment (
x += 1) instead ofx = x + 1— it’s shorter, and for mutable types it can avoid an unnecessary copy. - Be aware that
+=mutates lists, sets, and dicts in place; if two variables reference the same list, mutating one via+=affects the other. Usex = x + [...]deliberately if you need a fresh object instead. - Add parentheses to clarify precedence even when not strictly required —
(a and b) or cis easier to read at a glance thana and b or c. - Take advantage of chained comparisons (
0 <= x < 10) instead of0 <= x and x < 10— it’s idiomatic and the middle operand is only evaluated once. - Remember short-circuit evaluation when an operand has side effects or could raise an exception, e.g.
obj is not None and obj.value > 0safely guards againstNone. - Override
__eq__(and usually__hash__) on custom classes when equality should be based on data rather than identity, so==behaves intuitively.
Practice Exercises
- Exercise 1: Write a program that takes a rectangle’s
widthandheight, then uses arithmetic operators to print its area and perimeter. - Exercise 2: Write a function
is_leap_year(year)that uses comparison, logical, and modulo operators to returnTrueif the year is a leap year (divisible by 4, except centuries, which must be divisible by 400). - Exercise 3: Given two lists of the same length, use a loop and comparison/membership operators to count how many positions hold equal values, and separately check (with
is) whether the two list objects themselves are the same object.
Summary
- Operators combine operands into expressions; most are backed by dunder methods (
__add__,__eq__, etc.) that types can override. - Arithmetic operators include
+ - * /(true division, always float) plus//(floor division) and%(modulo). - Comparison operators (
== != < > <= >=) return booleans and can be chained; logical operators (and or not) use truthiness and short-circuit evaluation. is/is notcheck object identity;==/!=check value equality — they are not interchangeable.in/not intest membership in strings, lists, tuples, sets, and dicts.- Augmented assignment (
+=etc.) can mutate objects in place for mutable types, which is convenient but worth being deliberate about. - Operator precedence follows a fixed table (exponent highest, then unary, multiplicative, additive, comparisons, then logical), and parentheses always win.
