skip to content

Why must Python's `:=` be parenthesized in `if (n := len(s)) > 10:`?

level: middleimportance: must knowfreq 50%

answer

  1. It groups looser than you expect
  2. The condition still passes; the value is wrong
  3. Comparison wins, so a boolean gets stored
  4. Some positions require them regardless of grouping

basics

~20 s

:= binds looser than any other operator, so if n := len(s) > 10: parses as n := (len(s) > 10) and stores a boolean instead of the length. The parentheses force the binding to happen first.

solid answer

~50 s

Assignment expressions sit at the very bottom of Python's precedence table, below comparison and boolean operators, so an unparenthesized `n := len(s) > 10` groups as `n := (len(s) > 10)`. The condition still works — a non-empty string of eleven characters gives `True` — but `n` now holds a boolean rather than the length, which is a silent wrong-value bug rather than a `SyntaxError`. Wrapping the binding in parentheses, `(n := len(s)) > 10`, makes the length the bound value and the comparison the condition. Separately, the grammar *requires* parentheses in several positions regardless of precedence: an assignment expression cannot stand alone as a statement, cannot be the bare right-hand side of `=`, and cannot be the unparenthesized value of a keyword argument or a parameter default. Illegal targets — attributes, subscripts, unpacking — are rejected outright.

code

python · 7 lines
python
s = "hello world"

if n := len(s) > 10:
    print("without parens, n =", n)   # True

if (n := len(s)) > 10:
    print("with parens, n =", n)      # 11

go deeper

for a junior

Learn the safe shape by heart: put the whole name := value binding in its own parentheses, then compare outside them. That habit avoids the entire class of mistake.

for a middle

Explain the mechanics — := sits below comparison in the precedence table, so the unparenthesized form binds a boolean, and PEP 572 additionally forbids the bare form as a statement, as an assignment right-hand side, and as a keyword-argument value.

for a senior

Show that you would catch this in review by reading what gets bound, not just whether the branch works, and that you know short-circuiting behind and or or can leave the name unbound at runtime.

for a principal

Set the team position: require the parenthesised binding form in the style guide and lean on a linter for it, since the failure mode is a silent wrong value rather than an exception the test suite will surface.

### Two separate reasons parentheses appear People conflate them, and an interviewer will usually be probing whether you can pull them apart. One reason is **precedence**: the parentheses change what gets bound. The other is **grammar**: in certain positions PEP 572 requires the parentheses even though no ambiguity of meaning exists. ### Precedence — the silent wrong-value bug `:=` binds more loosely than every other operator; only the comma separates lower. So Python reads ```python if n := len(s) > 10: ``` as `n := (len(s) > 10)`. The comparison happens first, the boolean is bound to `n`, and *that* boolean is the condition. For a string of eleven characters the branch still runs, so the code looks correct in a quick test — but `n` is `True`, not `11`, and the moment the body uses `n` as a number you get either nonsense (`True` is 1 in arithmetic) or a type error further downstream. Nothing raises at compile time, which is what makes it a nastier bug than a `SyntaxError` would be. The fix is to say what you mean: ```python if (n := len(s)) > 10: print(n) # 11 ``` Now the binding is a self-contained expression, its value is the length, and the comparison is applied to that value. The same reading applies wherever a walrus meets an operator. `total := x + y` binds the sum, because `+` binds tighter — that one happens to be what you want. `found := x in seen` binds the boolean; `(found := x) in seen` binds the item. Whenever the intent is not obvious at a glance, the parentheses are worth writing even where they are redundant. ### Grammar — the positions where parentheses are mandatory PEP 572 deliberately prohibited the unparenthesized form in places where a reader could plausibly have meant `=`: * **Alone as a statement.** `x := 5` on its own line is a `SyntaxError`. This is the anti-typo rule: if `:=` were legal there, a stray colon would turn an intended assignment into an easily-missed one. * **The bare right-hand side of an assignment.** `y = x := 0` is rejected; `y = (x := 0)` is fine. * **The value of a keyword argument.** `f(mode=m := "RGB")` is rejected; `f(mode=(m := "RGB"))` is accepted. A *positional* argument needs no parentheses — `f(m := "RGB")` is legal — precisely because there is no `name=value` shape to confuse it with. * **A parameter default.** `def g(limit=(n := 10)): ...` needs the parentheses for the same reason. Note what these have in common: every one is a spot where `=` itself is meaningful, so the language insists you make the distinction visible. ### Illegal targets Precedence and parentheses cannot rescue an invalid target. The left side of `:=` must be a single plain identifier: * `obj.attr := 1` — `SyntaxError` * `items[0] := 1` — `SyntaxError` * `(a, b) := pair` — `SyntaxError` * there is no augmented form, so nothing corresponds to `total += 1` If you need any of those, the expression form is simply not available; use an assignment statement. ### The scope of the bound name is unaffected Parenthesising changes grouping, not binding rules. `(n := len(s))` inside a function makes `n` a local of that function, just as `n = len(s)` would. Importantly, the binding happens as soon as the sub-expression is evaluated — even when the surrounding condition turns out false and the block never runs. After `if (n := len(s)) > 10:` fails, `n` is still bound to the length. The flip side is short-circuiting: in `if flag and (n := compute()):`, a falsy `flag` means the right operand is never evaluated, so `n` is never bound, and a later read of `n` raises `UnboundLocalError`. That is a genuine production trap, not a curiosity — putting a walrus behind an `and` or an `or` makes the binding conditional. ### The practical rule Write the parentheses around the binding, always, even where precedence would have been kind to you. They cost two characters, they make the bound value unambiguous to the next reader, and they are mandatory in enough positions that the habit saves you from looking them up. On Python 3.14 all of this is unchanged from 3.8, when PEP 572 introduced the operator.

  • What is `n` bound to after `if n := len('hello') > 10:` runs?
    `False`. The comparison binds tighter, so the expression is `n := (len('hello') > 10)`, and five is not greater than ten. The binding still happens even though the branch does not run, so afterwards `n` is the boolean `False` rather than the integer 5. It is a value bug with no error message, which is why the parenthesised form is the idiom.
  • Does `f(m := 'RGB')` need parentheses the way `f(mode=m := 'RGB')` does?
    No. A positional argument accepts an unparenthesized assignment expression, because there is no `name=value` form to confuse it with. A keyword argument does have one, so PEP 572 requires `f(mode=(m := 'RGB'))` to keep `:=` and `=` visually distinct at that position. The same requirement applies to parameter defaults in a `def`.
  • If the `if` condition is false, is the walrus-bound name still set?
    Yes, provided the sub-expression was actually evaluated — binding happens during evaluation, not on branch entry. The exception is short-circuiting: in `if flag and (n := compute()):` a falsy `flag` means the right operand never runs, so `n` is never bound and a later read raises `UnboundLocalError`. Avoid putting a walrus behind `and` or `or` when you need the name afterwards.

saying these in an interview costs you the question

  • Says the unparenthesized form is a SyntaxError
  • Assumes `n` holds the length either way
  • Thinks `:=` binds tighter than comparison operators
  • Claims parentheses change which scope the name lands in
  • Believes a false condition means the name is unbound
  • Thinks a positional argument also needs the parentheses

context