Python Module 2 – Values, variables, numbers and strings
Operators, precedence and the walrus
Read Python expressions the way the parser does: operator precedence and grouping, augmented assignment, membership tests with in, and the := walrus.
What you will learn
- Apply precedence rules and add brackets for clarity
- Use augmented assignment and the := assignment expression
- Test membership with in and not in
Before you start
On this page
2 + 3 * 4 ** 2 has exactly one meaning to Python, and two rules decide it. Precedence says which operators bind
more tightly than others, and grouping says which way a run of operators of the same level is read. This lesson
gives the whole order, shows how to see the grouping the parser actually uses, and then covers the operators that
assign: augmented assignment and the walrus, :=.
The order of the operators
From the most tightly binding to the least:
| Operators | What they are |
|---|---|
(…), […], {…} |
brackets, and list, dictionary and set displays |
x[i], x(…), x.name |
indexing and slicing, calls, attributes |
await x |
waiting in asynchronous code (a later module) |
** |
power |
+x, -x, ~x |
unary plus and minus, bitwise not |
*, @, /, //, % |
multiplication (@ is matrix multiplication), division, remainder |
+, - |
addition and subtraction |
<<, >> |
bit shifts |
& |
bitwise and |
^ |
bitwise exclusive or |
| |
bitwise or |
in, not in, is, is not, <, <=, >, >=, !=, == |
comparisons, all on one level |
not x |
Boolean not |
and |
Boolean and |
or |
Boolean or |
x if condition else y |
conditional expression |
lambda |
anonymous function (the functions module) |
:= |
assignment expression |
Operators on the same level are read from left to right, so 100 / 10 / 5 is (100 / 10) / 5. There are two
exceptions: ** and the conditional expression are read from right to left. Comparisons chain instead, as the
previous lesson showed. Each line of this program prints an expression and then the same expression with every
bracket Python adds:
# Each line prints an expression, then the same expression with the brackets Python adds.
print(2 + 3 * 4, 2 + (3 * 4)) # * before +
print(-3 ** 2, -(3 ** 2)) # ** before the minus sign on its left
print(2 ** 3 ** 2, 2 ** (3 ** 2)) # ** groups from right to left
print(2 ** -1, 2 ** (-1)) # a minus sign on the right belongs to the exponent
print(100 / 10 / 5, (100 / 10) / 5) # the other operators group from left to right
print(-7 // 2, (-7) // 2) # the sign binds tighter than //
print(1 + 2 < 4, (1 + 2) < 4) # arithmetic before comparison
print(not 1 == 2, not (1 == 2)) # comparison before not
print(True or False and False, True or (False and False)) # and before or
print(1 + 2 if False else 3 + 4, (1 + 2) if False else (3 + 4)) # the conditional comes last Output
14 14 -9 -9 512 512 0.5 0.5 2.0 2.0 -4 -4 True True True True True True 7 7
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grouping.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The pairs always agree. The rows most worth remembering:
-3 ** 2is-9: the power binds more tightly than the minus sign on its left, as in school algebra. Write(-3) ** 2to square -3.2 ** 3 ** 2is2 ** 9, or 512, because powers are read from the right. On its right, though, a minus sign belongs to the exponent:2 ** -1is 0.5.-7 // 2is(-7) // 2, which is -4;-(7 // 2)would be -3 (the floor division of the integers lesson).not 1 == 2isnot (1 == 2), because comparisons bind more tightly thannot;1 == not 2is a syntax error.andbinds more tightly thanor:a or b and cmeansa or (b and c).- The conditional expression comes almost last, so
1 + 2 if False else 3 + 4adds up each side first and is 7.
See the parser’s grouping for yourself
Before running an expression, Python parses it into a tree in which every operator joins its operands. The standard
library’s ast module shows that tree without running anything:
import ast
for text in ["2 + 3 * 4", "-3 ** 2"]:
tree = ast.parse(text, mode="eval") # parse the expression without running it
print(text)
print(ast.dump(tree.body, indent=2)) Output
2 + 3 * 4
BinOp(
left=Constant(value=2),
op=Add(),
right=BinOp(
left=Constant(value=3),
op=Mult(),
right=Constant(value=4)))
-3 ** 2
UnaryOp(
op=USub(),
operand=BinOp(
left=Constant(value=3),
op=Pow(),
right=Constant(value=2)))
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 parse_tree.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
In the first tree the * sits inside the +, which means it is worked out first. In the second, the minus sign
(USub) applies to the result of the power. A bigger example, drawn as a tree:
The parse tree of 2 + 3 * 4 ** 2
Text description of the diagram
The diagram is the tree Python builds for the expression 2 + 3 * 4 ** 2. Each operator is a node whose two branches are its operands.
- At the top is +, the operator with the lowest precedence. Its operands are 2 and everything to its right, 3 * 4 ** 2.
- Below it is *, whose operands are 3 and 4 ** 2.
- At the bottom is **, which binds most tightly. Its operands are 4 and 2.
Python works the tree out from the bottom up: 4 ** 2 gives 16, then 3 * 16 gives 48, then 2 + 48 gives 50. Written with every bracket, the expression is 2 + (3 * (4 ** 2)).
Brackets are for people
Precedence tells Python what an expression means, but your reader should not need the table to see it. Add brackets
wherever two kinds of operator meet and the order is not obvious: and with or, not with a comparison, a minus
sign with **. They cost nothing.
School arithmetic (BODMAS or PEMDAS) agrees with Python on the basics: multiplication and division share a level and are worked out from left to right, and so are addition and subtraction.
print(6 / 2 * (1 + 2)) # / and * share a level, left to right: (6 / 2) * 3
print(6 / (2 * (1 + 2))) # brackets make the other reading explicit
print(8 / 2 / 2) # (8 / 2) / 2, not 8 / (2 / 2) Output
9.0 1.0 2.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 school_maths.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
6 / 2 * (1 + 2) is 9.0 in Python: the division happens first, then the multiplication. If you meant 6 divided by
the product, the brackets have to say so, as on the second line.
A common mistake: multiplying without *
Maths writes 3(x + 1) for “3 times (x + 1)”. In Python, writing something in brackets straight after a value is a call, so it tries to call the number 3:
x = 2
print(3(x + 1)) # maths notation, but Python needs 3 * (x + 1) Output (exit status 1)
Printed as an error (standard error)
implicit_multiply.py:2: SyntaxWarning: 'int' object is not callable; perhaps you missed a comma?
print(3(x + 1)) # maths notation, but Python needs 3 * (x + 1)
Traceback (most recent call last):
File "implicit_multiply.py", line 2, in <module>
print(3(x + 1)) # maths notation, but Python needs 3 * (x + 1)
~^^^^^^^
TypeError: 'int' object is not callable
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 implicit_multiply.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The compiler already suspects a mistake and adds a SyntaxWarning before the program runs; the TypeError follows
when the line runs. Write 3 * (x + 1).
Another trap catches people who know C, which orders comparisons and bitwise operators differently. In Python every
comparison binds more loosely than every arithmetic, shift and bitwise operator, so x & 1 == 0 means
(x & 1) == 0, the test it looks like.
Augmented assignment
count += 5 works like count = count + 5, and every arithmetic and bitwise operator has such a form: -=, *=,
/=, //=, %=, **= and the rest. For numbers and strings, which cannot change, the name is simply rebound to
the result. For a list, += changes the list itself:
count = 10
count += 5 # count = count + 5
count //= 4 # count = count // 4
count **= 2 # count = count ** 2
print(count)
prices = [10, 20]
alias = prices
prices += [30] # += changes this list in place ...
print(prices, alias, prices is alias)
prices = prices + [40] # ... while + builds a new list
print(prices, alias, prices is alias) Output
9 [10, 20, 30] [10, 20, 30] True [10, 20, 30, 40] [10, 20, 30] False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 augmented.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
After prices += [30], alias sees the new item, because both names still refer to one list that was extended in
place. prices = prices + [40] builds a new list instead and moves only the name prices to it, the rebinding of the
variables lesson.
Python has no ++ or --. count++ is a syntax error, and ++count is worse, because it is valid: two unary plus
signs, which change nothing:
count = 5
print(++count) # two plus signs: +(+count), still 5
print(--count) # two minus signs: -(-count), also 5
count += 1 # the way to add one
print(count) Output
5 5 6
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 no_increment.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The walrus operator :=
name := expression assigns the value to the name and also gives the value back, so it can sit inside a larger
expression. PEP 572 added it in Python 3.8 under the name assignment expression; “walrus operator” is the
nickname it picked up while that proposal was being debated.
>>> (n := 10) * 2
20
>>> n
10
>>> (size := len("hello") > 3)
True
>>> size
True
>>> (size := len("hello")) > 3
True
>>> size
5
>>> n := 5
File "<python-input-6>", line 1
n := 5
^^
SyntaxError: invalid syntax This session was replayed with Python 3.14.8 on macOS 26 arm64, and it printed exactly what is shown.
The session shows the three rules that matter:
- The walrus has the lowest precedence of all, so it takes everything to its right:
size := len("hello") > 3stores the result of the comparison,True. Put the assignment in brackets when it is part of a bigger expression. - On its own line it is a syntax error. An ordinary assignment is a statement, so use
=there. - The name stays bound afterwards, like any other variable.
Its best use is a value that a condition needs and the code after it uses again. Reading lines until an empty one
needs input() twice without it, and once with it:
With := · sum_lines.py
total = 0
while (line := input()) != "": # read a line, name it, then test it
total += int(line)
print("Total:", total) Input (standard input)
250 120 30
Output
Total: 400
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sum_lines.py
Without := · sum_lines_without.py
total = 0
line = input()
while line != "":
total += int(line)
line = input() # the same call again, at the end of the loop
print("Total:", total) Input (standard input)
250 120 30
Output
Total: 400
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sum_lines_without.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
PEP 572 itself recommends the plain assignment statement whenever both forms would work, because a statement says
most clearly what it does. Use := to remove a repeated call or computation, as here, and not to squeeze more into one
line.
Membership tests: in and not in
x in y asks whether y contains x, and x not in y asks the opposite:
print("ell" in "hello") # a substring
print("" in "hello") # the empty string is in every string
print(3 in [1, 2, 3], 4 not in [1, 2, 3])
marks = {"Asha": 91, "Ravi": 78}
print("Asha" in marks) # a dict is searched by its keys ...
print(91 in marks, 91 in marks.values()) # ... and by its values only if you ask
print(not 3 in [1, 2], 3 not in [1, 2]) # the same test; not in reads better Output
True True True True True False True True True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 membership.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- For strings,
inlooks for a substring, and the empty string is part of every string. - For lists and tuples, it looks for an item equal to
x. - For a dictionary, it looks at the keys only; search
marks.values()when you mean the values. x not in ymeansnot (x in y)and reads more naturally, so prefer it.
How long in takes depends on the container. A list or a tuple is checked item by item, so the time grows with its
length; a set or a dictionary finds an item through its hash, on average in about the same time however large it
is. If a program tests membership again and again against the same collection, make that collection a set.
Key takeaways
- After brackets, indexing, calls and
await,**binds most tightly and groups from the right; then come the unary signs,* / // %,+ -, the bitwise operators, the comparisons,not,and,or, the conditional expression,lambdaand:=. - Operators on one level group from left to right, except
**and the conditional expression; comparisons chain. - Brackets cost nothing: add them where
andmeetsor, or a minus sign meets**. x += yrebinds numbers and strings but changes a list in place; there is no++.:=assigns inside an expression, has the lowest precedence and usually needs brackets; prefer=when either would do.insearches strings for substrings, lists for items and dictionaries by key; sets and dictionaries answer fast.
Exercise
Exercise · Easy · Python
Write the leap-year rule as one expression
In the Gregorian calendar, a year is a leap year when it is divisible by 4, except that a year divisible by 100 is a leap year only when it is also divisible by 400. So 2024 and 2000 are leap years, while 2023 and 1900 are not.
Complete is_leap(year) in leap.py. The starter code handles only the first part of the rule, so it wrongly calls 1900 a leap year. Keep the function to a single return statement whose expression uses %, ==, !=, and and or, and add brackets wherever they make it easier to read, even where Python does not need them.
The sample tests check famous years, compare your function with the standard library's calendar.isleap() for every year from 1 to 3000, and read your file to make sure it is one return statement that does not use the calendar module itself.
Starter code · leap.py
def is_leap(year):
"""Return True if year is a leap year in the Gregorian calendar, otherwise False."""
return year % 4 == 0 # a first draft: 1900 is not a leap year The sample tests · test_leap.py
import ast
import calendar
import leap
from leap import is_leap
def test_famous_years():
"""gets the well-known years right"""
assert is_leap(2024) is True
assert is_leap(2023) is False
assert is_leap(2000) is True
assert is_leap(1900) is False
assert is_leap(2100) is False
assert is_leap(2400) is True
def test_every_year():
"""agrees with calendar.isleap() for every year from 1 to 3000"""
wrong = [year for year in range(1, 3001) if is_leap(year) != calendar.isleap(year)]
assert wrong == []
def test_one_expression():
"""is one return statement and does not use the calendar module"""
with open(leap.__file__, encoding="utf-8") as source:
tree = ast.parse(source.read())
function = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "is_leap"][0]
body = [node for node in function.body if not (isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant))]
assert len(body) == 1 and isinstance(body[0], ast.Return), "the body should be a single return statement"
modules = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
modules.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom):
modules.add(node.module)
assert "calendar" not in modules, "work the rule out with %, without the calendar module" A hint
Write each part of the rule as a comparison first: year % 4 == 0 means "divisible by 4", and year % 100 != 0 means "not divisible by 100". Then join the parts with and and or. and binds more tightly than or, so a and b or c means (a and b) or c: use brackets to show which parts belong together.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- Operator precedence (Python Software Foundation)
- The power operator (Python Software Foundation)
- Expressions: comparisons (Python Software Foundation)
- Assignment expressions (Python Software Foundation)
- Membership test operations (Python Software Foundation)
- Augmented assignment statements (Python Software Foundation)
- Time complexity of operations on built-in types (Python Software Foundation)
- ast, abstract syntax trees (Python Software Foundation)
- calendar.isleap() (Python Software Foundation)
- PEP 572: Assignment Expressions (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress