Skip to content
Unit 6 of 28

Operators and expressions

Contents · 19

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 /, //, %, ** and round(), including negative numbers and halfway cases
  • Write chained comparisons and explain when comparing different types raises TypeError
  • Use and, or and not for their short circuit behaviour, and avoid the x or default trap
  • 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

OperatorNameExampleResult
+, -, *addition, subtraction, multiplication7 * 214
/true division7 / 23.5
//floor division7 // 23
%remainder (modulo)7 % 21
**power7 ** 249
-x, +xunary 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 ** 2 means -(2 ** 2). Write (-2) ** 2 for the square of minus two.
  • ** groups from right to left: 2 ** 3 ** 2 is 2 ** 9, not 8 ** 2.
  • pow(base, exp, mod) computes (base ** exp) % mod without 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.3 is False. Use math.isclose(), as shown in Values, names and types. The special value float("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:

ExpressionReturns
x and yx if x is falsy, otherwise y
x or yx if x is truthy, otherwise y
not xTrue 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).

OperatorMeaningExampleResultIn binary
&AND: 1 where both bits are 1x & y81000
|OR: 1 where either bit is 1x | y141110
^XOR: 1 where the bits differx ^ y60110
~NOT: flips every bit, which equals -x - 1~x-13-1101
<<shift left by n: multiply by 2**nx << 248110000
>>shift right by n: floor divide by 2**nx >> 2311

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, so a > 0 & b > 0 means a > (0 & b) > 0, a chained comparison you didn't intend. Use and/or for 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 += 1 built a new int and rebound count. alias still refers to 10.
  • scores += [70] asked the list to extend itself. same names the same list, so it shows 70 too.
  • scores = scores + [60] built a new list and rebound scores only. same kept 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 := 5 on its own line is a SyntaxError, and in if (n := len(words)) > 3 the parentheses are what make n the 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 if statement (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.

OperatorsMeaning
(...), [...], {...}parentheses and displays
x[i], x[i:j], f(...), x.attrsubscription, slicing, call, attribute
await xawait (asynchronous code)
**power (groups right to left)
+x, -x, ~xunary 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 xboolean NOT
andboolean AND
orboolean OR
A if C else Bconditional expression
lambdalambda 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

  1. What are -9 // 4 and -9 % 4, and why?
  2. Why does round(2.5) give 2, and how would you round 2.5 half up?
  3. What do [] or "empty" and "x" and 0 evaluate to?
  4. Why is timeout = user_timeout or 30 risky, and what should you write instead?
  5. After a = [1]; b = a; a += [2], what is b? What is it if the last statement is a = a + [2] instead?
  6. What happens with 3 < "4"? With 3 == "3"?
  7. How does Python read x > 0 & y > 0?
  8. Why is n in big_set usually much faster than n in big_list?

Suggested answers

  1. -3 and 3. Floor division rounds towards negative infinity (−2.25 becomes −3), and the remainder makes -3 * 4 + 3 == -9 true. It has the sign of the divisor.
  2. round() rounds half to even, and 2 is the even neighbour. Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP) gives 3.
  3. "empty", because the first operand is falsy so or returns the second; and 0, because the first operand is truthy so and returns the second.
  4. A user's explicit 0 is falsy and gets replaced by 30. Write user_timeout if user_timeout is not None else 30.
  5. [1, 2], because += extends the shared list in place. With a = a + [2], b stays [1]: a new list is built and only a is rebound.
  6. 3 < "4" raises TypeError, since ints and strings have no ordering between them. 3 == "3" is False; for built in types, equality between unrelated types is not an error.
  7. As the chained comparison x > (0 & y) > 0, because & binds more tightly than >. Write x > 0 and y > 0.
  8. 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.

  1. Clock time. Write to_hms(seconds) that turns a nonnegative number of seconds into a tuple (hours, minutes, seconds) using divmod, and raises ValueError for a negative input. to_hms(3725) → (1, 2, 5); to_hms(86399) → (23, 59, 59); to_hms(0) → (0, 0, 0).
  2. Weekday shift. Days are numbered 0 (Monday) to 6 (Sunday). Write shift_weekday(day, offset) that returns the day offset days later, as a single expression; offset may be negative or larger than 7. shift_weekday(2, 3) → 5; shift_weekday(6, 1) → 0; shift_weekday(2, -10) → 6.
  3. 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 single return of a boolean expression. 2024 → True, 2023 → False, 1900 → False, 2000 → True.
  4. Permission flags. With READ, WRITE, EXECUTE = 4, 2, 1, write to_rwx(perms), which returns a three character string with r, w, x for set flags and - for clear ones, and revoke(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".
  5. Prices rounded half up. Write round_price(text) that takes a price as a decimal string and returns a Decimal rounded 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().