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 a bool.
  • Logicaland or not — combine boolean expressions.
  • Identityis, is not — check whether two names refer to the exact same object in memory.
  • Membershipin, 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; reserve is/is not for identity checks, especially is 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 of x = 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. Use x = x + [...] deliberately if you need a fresh object instead.
  • Add parentheses to clarify precedence even when not strictly required — (a and b) or c is easier to read at a glance than a and b or c.
  • Take advantage of chained comparisons (0 <= x < 10) instead of 0 <= 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 > 0 safely guards against None.
  • 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 width and height, 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 return True if 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 not check object identity; ==/!= check value equality — they are not interchangeable.
  • in/not in test 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.