Operators are the small symbols that make Python actually compute things: adding cart totals, comparing prices, checking whether a product is in stock, combining boolean conditions. This lesson covers every operator you'll use day-to-day, the rules Python follows when several appear in the same expression, and a few sharp edges that trip up beginners.
These do the same thing they did in school, with two extras worth knowing about: floor division and exponentiation.
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | 19.99 + 4.99 | 24.98 |
- | Subtraction | 100 - 19.99 | 80.01 |
* | Multiplication | 19.99 * 3 | 59.97 |
/ | True division | 10 / 3 | 3.3333333333333335 |
// | Floor division | 10 // 3 | 3 |
% | Modulo (remainder) | 10 % 3 | 1 |
** | Exponentiation | 2 ** 10 | 1024 |
Here they are working on a small cart:
Two things deserve a closer look: / versus //, and what % actually does.
/ always returns a float, even when the numbers divide evenly. 10 / 2 is 5.0, not 5. If you want an integer result, use //, which discards the fractional part and rounds toward negative infinity:
That last result surprises people. -10 // 3 is -4, not -3, because floor division rounds down (toward negative infinity), not toward zero. If you want truncation toward zero, use int(-10 / 3) instead, which gives -3.
% returns the remainder after division. It's useful for splitting items into groups or detecting "every Nth" events:
** is exponentiation. 2 ** 10 is 1024, 9 ** 0.5 is 3.0. It also works with negative exponents to give fractions: 2 ** -1 is 0.5.
Cost: ** with huge integer exponents can produce arbitrarily large results (Python ints have no fixed size). 2 ** 1000 is fine, but 10 ** 1_000_000 will allocate megabytes of memory and take a noticeable moment. Use it deliberately.
Comparison operators return True or False. You use them everywhere: filtering products by price, checking stock, deciding which orders qualify for free shipping.
| Operator | Meaning | Example | Result |
|---|---|---|---|
== | Equal to | 19.99 == 19.99 | True |
!= | Not equal to | 19.99 != 24.99 | True |
< | Less than | 5 < 10 | True |
<= | Less than or equal | 5 <= 5 | True |
> | Greater than | 10 > 5 | True |
>= | Greater than or equal | 10 >= 10 | True |
A common beginner mistake is using = (assignment) when you meant == (comparison). Python will refuse to run code like if cart_total = 50: and raise a SyntaxError, which is helpful. The two symbols do completely different things: = puts a value into a name, == asks whether two values are equal.
Comparisons also work on strings (lexicographic order) and other types, but mixing types you can't compare raises an error:
String comparison goes character by character using Unicode code points, so "apple" comes before "banana". Comparing a number to a string makes no sense to Python and raises TypeError.
Python has a feature most other languages don't: you can chain comparison operators in a single expression and Python evaluates them mathematically.
20 < price < 100 reads exactly like math notation and means 20 < price and price < 100. Python evaluates price once and combines both comparisons.
Here's the flow Python actually follows when it sees a chained comparison:
The key insight is that price is computed exactly once and then reused for both comparisons. If the first check fails, Python doesn't even bother evaluating the second one, which is the same short-circuit behavior and uses everywhere else.
You can chain as many as you want, and they can mix operators:
The second one means "1 < rating AND rating < 5 AND 5 != 0", which is True because all three are true.
Without chaining, you'd write the awkward version:
That works fine, but the chained form is more readable and slightly faster because price is evaluated only once.
Cost: In a < f() < b, Python calls f() exactly once. With a < f() and f() < b, it calls f() twice. If f() is expensive, chaining matters.
Logical operators combine boolean values. Python uses words (and, or, not) instead of symbols like && and ||.
| Operator | Meaning | Example | Result |
|---|---|---|---|
and | True if both are true | True and False | False |
or | True if either is true | True or False | True |
not | Inverts the value | not True | False |
Here's a cart check that combines two conditions:
Two behaviors are worth pinning down: short-circuit evaluation and the actual value these operators return.
and and or stop evaluating as soon as the answer is known.
False and X is always False, so Python never evaluates X.True or X is always True, so Python never evaluates X.This lets you guard against errors:
If discount_code is empty, the first half is False and Python skips the second half entirely. Without short-circuiting, calling .startswith("SAVE") on a value you haven't validated could be a bug source.
This diagram shows how or decides whether to evaluate its right operand, and what it actually returns (which is one of the operands, not a plain True or False):
and mirrors this exactly, just with the test flipped: it stops on the first falsy operand and returns it, otherwise it returns the right operand. That's why "" or "Guest" returns "Guest" (left is falsy, so it falls through) and "Alex" and "alex@example.com" returns the email (left is truthy, so and keeps going).
and and or Actually ReturnHere's a subtle point: and and or don't return True or False directly. They return one of the operands.
a and b returns a if a is falsy, otherwise b.a or b returns a if a is truthy, otherwise b.0 or 19.99 returns 19.99 because 0 is falsy. "" or "Guest" falls back to "Guest" for the same reason, which is a common idiom for default values. "Alex" and "alex@example.com" returns the second value because the first is truthy and and keeps going.
That's why you often see code like name = user_name or "Guest" in Python: it uses the actual value when it exists and falls back when it doesn't.
not is simpler. It always returns a real True or False:
not "" is True because the empty string is falsy. not 19.99 is False because any non-zero number is truthy.
is vs ==== asks "are these values equal?". is asks "are these the same object in memory?". They look similar but mean different things.
cart_a and cart_b have equal contents, so == is True. They're two separate lists in memory though, so is is False. cart_c was assigned from cart_a, so both names point to the exact same list, and is is True.
The rule of thumb: use == for value comparison, and reserve is for comparing with None, True, or False:
The official Python style guide (PEP 8) says to compare with None using is and is not, not ==. There's only ever one None object in a running Python program, so identity is the right test.
is not is the negated form. selected_coupon is not None means "the coupon exists".
A trap that catches everyone eventually: small integers and short strings are often interned (reused) by the interpreter, which makes is accidentally work for them in some cases and not others:
This is an implementation detail of CPython. Don't rely on it. Use == for values and is only for singletons.
in and not inin checks whether a value appears in a collection. It works for strings, lists, tuples, sets, dictionaries, and other iterables.
For strings, in checks substring containment:
For dictionaries, in checks the keys, not the values:
12 is a value in the dictionary, but in only looks at keys, so it returns False. If you actually want to search values, use 12 in stock.values().
Cost: x in list is O(n) because Python scans every element. x in set and x in dict are O(1) on average because both use hash tables. If you're checking membership repeatedly against a large collection, convert it to a set first.
Bitwise operators work on the binary representation of integers. You won't reach for them often in everyday application code, but they show up in low-level work: feature flags, permission bits, color manipulation, hash tricks.
| Operator | Name | Example | Result |
|---|---|---|---|
& | AND | 0b1100 & 0b1010 | 0b1000 (8) |
| | OR | 0b1100 | 0b1010 | 0b1110 (14) |
^ | XOR | 0b1100 ^ 0b1010 | 0b0110 (6) |
~ | NOT | ~0b1100 | -13 |
<< | Left shift | 1 << 4 | 16 |
>> | Right shift | 16 >> 2 | 4 |
A practical case: storing customer notification preferences as flags. Each preference is one bit.
| combines flags. & checks whether a flag is set. The result of preferences & EMAIL is either 0 (false) or the flag's value (truthy), which is why bool(...) produces the right answer.
<< and >> shift bits left and right. Left-shifting by n is equivalent to multiplying by 2 ** n, right-shifting divides by 2 ** n (floor):
~ flips every bit. For a Python integer, the result is -(x + 1) because of how negative numbers are represented in two's complement. ~5 is -6, ~0 is -1.
These operators only work on integers. Using them on floats raises TypeError. If you don't need bit-level work, you can comfortably skip them for now.
You already know =: it binds a value to a name. The augmented assignment operators combine an arithmetic or bitwise operator with assignment.
| Operator | Equivalent to | Example |
|---|---|---|
+= | x = x + y | total += price |
-= | x = x - y | stock -= 1 |
*= | x = x * y | price *= 1.1 |
/= | x = x / y | share /= 4 |
//= | x = x // y | groups //= 5 |
%= | x = x % y | index %= 10 |
**= | x = x ** y | value **= 2 |
&=, |=, ^=, <<=, >>= | Bitwise variants | flags |= ADMIN |
Building a cart total step by step:
For numbers and strings (which are immutable), x += y and x = x + y produce the same result. For mutable types like lists, there's a subtle difference: += modifies the existing list in place, while x = x + y creates a new list.
Both names see the new item because += on a list mutates the original list, and cart_b points to the same one. Compare with this version:
cart_a + ["USB Cable"] builds a new list and rebinds cart_a to it. cart_b still points to the original, so it doesn't change. This difference matters a lot once you start passing lists into functions.
Cost: Building a string with += in a loop is O(n^2) because strings are immutable, so each += creates a brand new string and copies the old contents over. Use "".join(parts) or an f-string for that pattern.
When an expression mixes operators, Python evaluates them in a specific order. Higher precedence binds tighter, meaning that operator runs first.
| Precedence | Operators | Notes |
|---|---|---|
| Highest | ** | Right-to-left associative |
+x, -x, ~x | Unary plus, minus, bitwise NOT | |
*, /, //, % | Multiplicative | |
+, - | Additive | |
<<, >> | Bitwise shifts | |
& | Bitwise AND | |
^ | Bitwise XOR | |
| | Bitwise OR | |
==, !=, <, <=, >, >=, is, is not, in, not in | All comparison and membership operators | |
not | Logical NOT | |
and | Logical AND | |
| Lowest | or | Logical OR |
The full table in the official docs has a few more rows for things you'll meet later (lambda, conditional expressions), but the above covers everything in this lesson.
Two quick examples:
In 2 + 3 * 4, the multiplication runs first (higher precedence), so it's 2 + 12. Parentheses force the addition first.
Mixing arithmetic, comparison, and logical operators in one line:
Comparison operators bind tighter than and, so Python evaluates price > 20 (True) and stock > 0 (True) first, then combines them with and. You don't need parentheses, but adding them never hurts readability:
One detail that confuses people: ** is right-associative.
2 ** 3 ** 2 means 2 ** (3 ** 2), which is 2 ** 9 = 512, not (2 ** 3) ** 2 = 64. If you're nesting exponentiation, use parentheses to be explicit.
When you're unsure, parentheses are free. They cost nothing at runtime and make intent obvious.
10 quizzes