An expression is any piece of code that produces a value: 3, price * 1.2, name or "guest". An operator is the symbol or keyword that combines values, called its operands, into a new value. This unit covers every operator family in Python, together with the corner cases that surprise people who know another language: how // and % treat negative numbers, why and and or don't always give True or False, and why += on a list behaves differently from += on a number.
These rules are small, but many "Python is weird" moments in later units trace back to one of them.
You will be able to:
- Predict the result of
/,//,%,**andround(), including negative numbers and halfway cases - Write chained comparisons and explain when comparing different types raises
TypeError - Use
and,orandnotfor their short circuit behaviour, and avoid thex or defaulttrap - Read bitwise expressions and say when
+=changes an object in place - Use the walrus operator
:=, membership tests and the conditional expression where they make code clearer - Use the precedence table, and add parentheses where the order isn't obvious
Arithmetic operators
| Operator | Name | Example | Result |
|---|---|---|---|
+, -, * | addition, subtraction, multiplication | 7 * 2 | 14 |
/ | true division | 7 / 2 | 3.5 |
// | floor division | 7 // 2 | 3 |
% | remainder (modulo) | 7 % 2 | 1 |
** | power | 7 ** 2 | 49 |
-x, +x | unary minus and plus | -7 | -7 |
/ always returns a float, even when the division is exact. // divides and then rounds down; it returns an int when both operands are ints and a float as soon as one is a float. Mixing an int and a float in any arithmetic gives a float.
Negative numbers: floor, not truncation
"Rounds down" means towards negative infinity. For negative results that differs from C, Java or JavaScript, which cut the fractional part off (they truncate towards zero).
Two rules explain every line. First, a == (a // b) * b + a % b always holds: with a = -7 and b = 3, -3 * 3 + 2 is -7. Second, the result of % has the sign of the divisor b, or is zero. This makes % ideal for wrapping around: (day + shift) % 7 is always between 0 and 6, even when shift is negative. If you need C style truncation, math.fmod(-7, 2) gives -1.0 and math.trunc(-3.5) gives -3.
All three division operators, % included, raise ZeroDivisionError: division by zero when the right operand is zero (Python 3.13 words the message a little differently for // and %). Errors and exceptions shows how to handle it.
Powers and divmod
**binds more tightly than the unary minus on its left, so-2 ** 2means-(2 ** 2). Write(-2) ** 2for the square of minus two.**groups from right to left:2 ** 3 ** 2is2 ** 9, not8 ** 2.pow(base, exp, mod)computes(base ** exp) % modwithout building the huge intermediate number.divmod(a, b)returns(a // b, a % b)in one call: 200 seconds is 3 minutes and 20 seconds.
round() and banker's rounding
round() rounds an exact halfway value to the nearest even digit. This is called "round half to even" or banker's rounding, and it avoids the upward bias you get when you add up many values that were each rounded half up. With one argument, round() returns an int; with a second argument, ndigits, it returns the same type as its input. A negative ndigits rounds to tens, hundreds and so on.
round(2.675, 2) gives 2.67 for a different reason: the float closest to 2.675 is slightly smaller than 2.675, so there is no halfway case at all (see 0.1 + 0.2 in Values, names and types). When the rounding rule matters, as it does with money, use decimal.Decimal built from a string and choose the rule explicitly:
Comparisons
The comparison operators are ==, !=, <, <=, > and >=, and each returns True or False. They can be chained: a < b < c means a < b and b < c, except that b is evaluated only once and Python stops at the first false link.
Numbers compare by value across types, so 1 == 1.0 is true. For the built in types, equality between unrelated types is not an error; it is False. Lists, tuples and strings compare item by item from the left, like words in a dictionary, and strings compare by Unicode code point, which puts every ASCII capital letter before every lowercase one.
Ordering values that have no natural order raises TypeError. This is common when numbers arrive as text from input() or a file; convert first, as in 3 < int("4").
The last line of the traceback reads TypeError: '<' not supported between instances of 'int' and 'str'.
Pitfall. Don't compare floats with
==after arithmetic:0.1 + 0.2 == 0.3isFalse. Usemath.isclose(), as shown in Values, names and types. The special valuefloat("nan")is not even equal to itself.
Boolean operators: and, or, not
Every object is truthy or falsy: 0, 0.0, "", None and empty containers are falsy, and almost everything else is truthy (Values, names and types). The boolean operators work on truthiness, and and and or return one of their operands, not necessarily a bool:
| Expression | Returns |
|---|---|
x and y | x if x is falsy, otherwise y |
x or y | x if x is truthy, otherwise y |
not x | True if x is falsy, otherwise False (always a bool) |
Short circuit evaluation
and and or evaluate their right operand only when they need it; this is called short circuiting. The block below uses a tiny function (see Functions) that announces when it runs:
The right side never ran. That makes guards such as if items and items[0] > 0: safe: when items is empty, items[0] is never evaluated, so there is no IndexError.
The x or default idiom and its trap
Because or returns its first truthy operand, name or "guest" is a short way to supply a fallback. But it treats every falsy value as missing, including 0 and "", which are often valid:
The user asked for zero retries, and the first version silently replaced that with 3. Use x or default only when every falsy value really means "not set". When only None means "not set", test for None explicitly.
Bitwise operators
Bitwise operators work on the binary digits (bits) of integers. In the table, x = 12 (binary 1100) and y = 10 (binary 1010).
| Operator | Meaning | Example | Result | In binary |
|---|---|---|---|---|
& | AND: 1 where both bits are 1 | x & y | 8 | 1000 |
| | OR: 1 where either bit is 1 | x | y | 14 | 1110 |
^ | XOR: 1 where the bits differ | x ^ y | 6 | 0110 |
~ | NOT: flips every bit, which equals -x - 1 | ~x | -13 | -1101 |
<< | shift left by n: multiply by 2**n | x << 2 | 48 | 110000 |
>> | shift right by n: floor divide by 2**n | x >> 2 | 3 | 11 |
Python ints have no fixed width, so think of a negative number as having infinitely many leading 1 bits. bin() shows a minus sign and the magnitude instead, which is why bin(~12) is -0b1101. A common use is a set of on/off flags packed into one integer:
| switches flags on, & flag tests one, and &= ~flag clears one. The same symbols work on sets as union, intersection and so on (Collections and comprehensions).
Pitfall.
&and|bind more tightly than comparisons, soa > 0 & b > 0meansa > (0 & b) > 0, a chained comparison you didn't intend. Useand/orfor conditions. When you must use&(NumPy and pandas masks do), put each comparison in parentheses.
Assignment and augmented assignment
Plain assignment = binds a name to an object; Values, names and types covers multiple assignment, swapping and unpacking. Augmented assignment combines an operator with assignment: +=, -=, *=, /=, //=, %=, **=, &=, |=, ^=, <<=, >>= and @=.
x += 1 is often described as "the same as x = x + 1". For immutable objects such as numbers, strings and tuples that is true. For mutable objects that support it, such as lists, += modifies the object in place, and every other name for that object sees the change:
- An int can't change, so
count += 1built a new int and reboundcount.aliasstill refers to 10. scores += [70]asked the list to extend itself.samenames the same list, so it shows 70 too.scores = scores + [60]built a new list and reboundscoresonly.samekept the old one.
This difference matters when you pass a list to a function (Functions). One more surprise: list += accepts any iterable. If letters is a list, letters += "ab" appends 'a' and 'b', while letters + "ab" raises TypeError.
Assignment expressions: the walrus :=
Since Python 3.8, name := value assigns and produces the value, so you can use it inside a larger expression. It is nicknamed the walrus operator after its shape. Its best use is a loop that fetches a value, tests it and uses it. Here io.StringIO acts as a small in memory text file:
Without := you would call stream.read(4) once before the loop and again at the end of its body. You can also use it in comprehension filters (Collections and comprehensions).
Note. A walrus needs parentheses in most places.
x := 5on its own line is aSyntaxError, and inif (n := len(words)) > 3the parentheses are what makenthe length rather than the result of the comparison. Use it when it removes a repetition, not to squeeze code onto one line.
Identity and membership
is and is not test whether two names refer to the same object, while == tests whether two values are equal (Values, names and types). In everyday code the main use of is is checking for None: write result is None, not result == None.
in and not in test membership, and what "member" means depends on the container:
On a string, in looks for a substring. On a list or tuple, it looks for an element equal to the value, so [1, 2] is not in [1, 2, 3]. On a dict, it checks the keys only; use .values() to search the values.
The cost differs too. A list is scanned from the start, so the work grows with its length; this is written O(n). Sets and dicts find an item by its hash in about the same time however large they are: O(1) on average. Your output will differ from this run, but the gap will be huge:
If you test membership many times against the same large collection, convert it to a set once.
The conditional expression
A if condition else B evaluates to A when the condition is truthy and to B otherwise; only the chosen branch is evaluated. Older code sometimes fakes it with condition and A or B, which breaks whenever A is falsy:
Tip. A conditional expression chooses a value. To choose between actions, or when you are tempted to nest one inside another, write an
ifstatement (Control flow).
The @ operator
@ is the matrix multiplication operator, added in Python 3.5 by PEP 465. No built in type implements it, so 1 @ 2 raises TypeError. It exists for libraries such as NumPy, where a @ b multiplies two arrays as matrices (Working with data). The @name line above a function definition is unrelated: it is a decorator (Functional tools and decorators).
Operator precedence
When an expression mixes operators, precedence decides which binds first. The table runs from the most tightly binding at the top to the least at the bottom; operators in the same row have equal precedence.
| Operators | Meaning |
|---|---|
(...), [...], {...} | parentheses and displays |
x[i], x[i:j], f(...), x.attr | subscription, slicing, call, attribute |
await x | await (asynchronous code) |
** | power (groups right to left) |
+x, -x, ~x | unary plus, minus, bitwise NOT |
*, @, /, //, % | multiplication, matrix multiplication, divisions, remainder |
+, - | addition, subtraction |
<<, >> | shifts |
& | bitwise AND |
^ | bitwise XOR |
| | bitwise OR |
in, not in, is, is not, <, <=, >, >=, !=, == | comparisons, membership, identity (these chain) |
not x | boolean NOT |
and | boolean AND |
or | boolean OR |
A if C else B | conditional expression |
lambda | lambda expression |
:= | assignment expression |
not 1 == 2 is not (1 == 2), because comparisons bind more tightly than not. and binds more tightly than or, so the middle value is True or (False and False). Most operators group from left to right (10 - 4 - 3 is (10 - 4) - 3); ** is the exception, and 2 ** -1 works because ** binds less tightly than a unary operator on its right. Operands are evaluated from left to right.
You don't need to memorise the table. When an expression mixes and with or, or bitwise operators with comparisons, add parentheses: they cost nothing and help the next reader.
Check yourself
- What are
-9 // 4and-9 % 4, and why? - Why does
round(2.5)give2, and how would you round 2.5 half up? - What do
[] or "empty"and"x" and 0evaluate to? - Why is
timeout = user_timeout or 30risky, and what should you write instead? - After
a = [1]; b = a; a += [2], what isb? What is it if the last statement isa = a + [2]instead? - What happens with
3 < "4"? With3 == "3"? - How does Python read
x > 0 & y > 0? - Why is
n in big_setusually much faster thann in big_list?
Suggested answers
-3and3. Floor division rounds towards negative infinity (−2.25 becomes −3), and the remainder makes-3 * 4 + 3 == -9true. It has the sign of the divisor.round()rounds half to even, and 2 is the even neighbour.Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP)gives 3."empty", because the first operand is falsy soorreturns the second; and0, because the first operand is truthy soandreturns the second.- A user's explicit
0is falsy and gets replaced by 30. Writeuser_timeout if user_timeout is not None else 30. [1, 2], because+=extends the shared list in place. Witha = a + [2],bstays[1]: a new list is built and onlyais rebound.3 < "4"raisesTypeError, since ints and strings have no ordering between them.3 == "3"isFalse; for built in types, equality between unrelated types is not an error.- As the chained comparison
x > (0 & y) > 0, because&binds more tightly than>. Writex > 0 and y > 0. - A set finds an element by its hash in about constant time (O(1) on average); a list has to be scanned element by element (O(n)).
Exercises
These exercises are optional practice. Pick the ones that help you.
The solutions wrap each answer in a small function and check it with assert, which stops the program if its condition is false. Functions and Errors and exceptions cover both properly.
- Clock time. Write
to_hms(seconds)that turns a nonnegative number of seconds into a tuple(hours, minutes, seconds)usingdivmod, and raisesValueErrorfor a negative input.to_hms(3725)→(1, 2, 5);to_hms(86399)→(23, 59, 59);to_hms(0)→(0, 0, 0). - Weekday shift. Days are numbered 0 (Monday) to 6 (Sunday). Write
shift_weekday(day, offset)that returns the dayoffsetdays later, as a single expression;offsetmay be negative or larger than 7.shift_weekday(2, 3)→5;shift_weekday(6, 1)→0;shift_weekday(2, -10)→6. - Leap year in one expression. A year is a leap year if it is divisible by 4, except century years, which must also be divisible by 400. Write
is_leap(year)whose body is a singlereturnof a boolean expression. 2024 →True, 2023 →False, 1900 →False, 2000 →True. - Permission flags. With
READ, WRITE, EXECUTE = 4, 2, 1, writeto_rwx(perms), which returns a three character string withr,w,xfor set flags and-for clear ones, andrevoke(perms, flag), which clears one flag.to_rwx(6)→"rw-";to_rwx(5)→"r-x";to_rwx(0)→"---";to_rwx(revoke(7, WRITE))→"r-x". - Prices rounded half up. Write
round_price(text)that takes a price as a decimal string and returns aDecimalrounded to two places, halves rounded up."2.675"→Decimal("2.68");"0.125"→Decimal("0.13");"1.004"→Decimal("1.00").
Solutions
1. Clock time.
2. Weekday shift. Because % takes the sign of the divisor, the result stays between 0 and 6 even for negative offsets; in C or Java you would need an extra + 7.
3. Leap year in one expression. The parentheses make the grouping explicit. For this particular test the result happens to be the same without them, but do not rely on that: and binds more tightly than or, so a mix without parentheses is easy to misread. The standard library has the same check as calendar.isleap().
4. Permission flags. Revoking a flag that isn't set leaves the value unchanged, because & ~flag only ever clears that one bit.
5. Prices rounded half up. Build the Decimal from a string: Decimal(2.675) would capture the float's tiny error and round down to 2.67, the same as round().