Walrus Operator¶
The walrus operator := allows assignment within expressions:
# Capture value in conditional
if (match := pattern.search(text)) is not None:
print(f"Found match at {match.start()}")
# Reuse computed value
results = [y for x in data if (y := transform(x)) is not None]
# Avoid repeated calls
while (line := file.read_line()) is not None:
process(line)
Re-evaluation in while tests. A walrus anywhere inside a while test is re-evaluated on every iteration, whatever expression hosts it — a member call on the bound value, an index, a keyword argument, a container element, a conditional branch, or a nested call all behave the same way:
def main():
xs: list[str] = [" a ", " b ", " "]
while (s := xs.pop(0)).strip():
print(s.strip())
print("done", len(xs))
Output (identical to Python):
Before this was fixed, the walrus was hoisted once ahead of the loop for every host other than a direct comparison, so the example above looped forever (#1723).
Value-Typed (R-V):
A walrus is a store followed by a read of its target; its type is what the target reads as after
the store. When the target is a wrapper type (T? or T | None), the walrus's type reflects the
stored value, not the wrapper:
def main() -> None:
m: int | None = None
n: int = (m := 5) # m stores 5, reads as int; n = 5
print(n)
Some(…) and None() take the target's declared slot, so a walrus that stores an Optional reads
as the declared type:
def main() -> None:
x: int? = None()
y: int? = (x := Some(2)) # stores Some(2); reads as int?
print(y)
Fresh Walrus Under a Slot (R-AE):
A fresh walrus (one that creates a new binding) directly under a store slot takes the slot's type. Without a slot, a bare None walrus is refused — None has no type on its own:
def g(s: str | None):
print(s)
g((q := None)) # q takes str | None from the parameter slot
print(q) # None
A fresh walrus whose value is a void call (a function that returns nothing) is always refused:
Type Inference Only:
The walrus operator always infers the type from the right-hand side expression. Type annotations are not supported with := (matching Python 3.8+ behavior):
# ✅ Valid - type inferred from get_value()
if (x := get_value()) > 0:
pass
# ❌ Invalid - cannot annotate with walrus
if (x: int := get_value()) > 0: # ERROR: type annotation not supported with :=
pass
Since Sharpy has full static type information, the type of get_value() is known at compile time, making explicit annotation unnecessary.
Walrus Operator in Comprehensions:
Variables assigned with := inside a comprehension are local to the comprehension and do not leak to the outer scope:
# Walrus is useful within a comprehension to avoid recomputation
results = [y * 2 for x in data if (y := transform(x)) > 0]
# y is used within the comprehension - valid!
# But y does NOT leak to outer scope
print(y) # ERROR: 'y' does not exist in this scope
# Same for iteration variables
print(x) # ERROR: 'x' does not exist in this scope
Departure from Python: In Python 3.8+, walrus assignments inside comprehensions leak to the containing scope. Sharpy deliberately differs here for cleaner semantics: the syntactic boundary ([...], {...}) equals the semantic boundary. Everything inside the comprehension delimiters stays inside.
If you need a value after the comprehension:
# Assign before the comprehension
items = get_items() # Not: [(x := get_items()) ...]
[x for x in items]
# Or use an explicit loop
last_valid: int | None = None
results: list[int] = []
for x in data:
y = transform(x)
if y > 0:
last_valid = y
results.append(y * 2)
Evaluation Placement¶
A walrus expression evaluates exactly where it is written — in a short-circuit branch, a
ternary arm, an elif test, a while test, a comparison-chain operand, a ?? right operand, a
lambda body, or a match guard. The compiler manufactures a sink at each of those constructs so a
hoisted lowering runs where the construct evaluates it.
The name a walrus binds has the lifetime of the enclosing scope, not of the enclosing
evaluation, so its declaration is placed at the enclosing scope even when its assignment is placed
inside a manufactured conditional or loop body. That is why a walrus declared in a while test is
readable in the loop's else clause and after the loop:
def probe(xs: list[int]) -> int:
print("probe", len(xs))
return len(xs)
def main() -> None:
xs: list[int] = [1, 2, 3]
while (n := probe(xs)) > 1:
xs.pop(0)
else:
print("else", n)
print("after", n)
def probe() -> int:
print("evaluated")
return 3
def main() -> None:
if False and (w := probe()):
pass
print("done")
The short-circuited and RHS is never evaluated.
In an or expression, the RHS evaluates only when the LHS is falsy:
def probe() -> int:
print("evaluated")
return 3
def main() -> None:
w: int = 0
if 0 or (w := probe()):
pass
print(f"w {w}")
In a while test, the walrus re-evaluates on every iteration:
def main() -> None:
xs: list[int] = [1, 2, 0, 3]
while (v := xs.pop(0)):
print(f"iter {v}")
print(f"done {len(xs)}")
Definite Assignment for Walrus Names¶
A walrus in a short-circuit branch may or may not execute — the name it binds is not necessarily assigned on all paths to a subsequent read. Sharpy applies the C# §9.4.4 when-true/when-false definite assignment rule:
- After
a and b: definitely assigned when true = T(a) ∪ T(b); when false = F(a) ∩ (T(a) ∪ F(b)). - After
a or b: definitely assigned when true = T(a) ∩ (F(a) ∪ T(b)); when false = F(a) ∪ F(b). - After
not e: true/false swap. - After
x if c else y: when true = (T(c) ∪ T(x)) ∩ (F(c) ∪ T(y)); when false = (T(c) ∪ F(x)) ∩ (F(c) ∪ F(y)). Only the condition runs unconditionally. - After a comparison chain
a < b < c: operands 0 and 1 always run, so their walruses are assigned on both outcomes; an operand past the first link runs only when every earlier link held, so it is assigned when true only. - After
a ?? b: onlyaruns unconditionally. - In a comprehension or generator expression: only the first
forclause's iterator is evaluated exactly once. A walrus in the element, the value, or anifclause runs once per item — zero times over an empty iterable — so it is not definitely assigned. python3 agrees:[(w := v) for v in []]followed by a read ofwraisesUnboundLocalError.
A walrus in a match guard is assigned on entry to the arm it guards, so the arm's body reads it:
def f() -> int:
print("f")
return 3
def main() -> None:
m: int = 1
match m:
case 1 if (w2 := f()) > 0:
print("one", w2)
case _:
print("other")
A read on a path where the walrus may not have executed is SPY0600, mirroring Python's
NameError:
def probe() -> int:
return 3
def main() -> None:
if True or (w := probe()):
print(w) # error SPY0600: variable 'w' is used before being assigned
A ternary arm and a comparison-chain operand past the first link refuse the same way:
def f() -> int:
return 3
def main() -> None:
c: bool = False
w: int
v: int = (w := f()) if c else 0
print(v, w) # error SPY0600: Variable 'w' is used before being assigned
def f() -> int:
return 3
def main() -> None:
w: int
if 5 < 1 < (w := f()):
print("in")
print("after", w) # error SPY0600: Variable 'w' is used before being assigned
python3 raises UnboundLocalError on both programs.
A walrus that is unconditional (e.g., the LHS of and) is available on both branches:
def probe() -> int:
return 1
def main() -> None:
if (w := probe()) > 0 and False:
pass
else:
print(f"else {w}")
w is definitely assigned regardless of the and result.
PEP 572 Prohibited Positions (SPY0704)¶
Following PEP 572, a walrus inside a comprehension's iterable expression is refused:
xs: list[int] = [1, 2, 3]
r = [y for y in (t := xs)] # error SPY0704: walrus operator not allowed in iterable
A walrus in the element or condition position of a comprehension is accepted:
Rebinding the iteration variable with a walrus in the element or condition is also refused:
xs: list[int] = [1, 2, 3]
r = [i := 0 for i in xs] # error SPY0704: walrus rebinds iteration variable 'i'
Walrus in F-String Holes¶
A walrus inside a parenthesized f-string hole is accepted — the parentheses distinguish it from a format specifier:
Without parentheses, {s := f()} would parse : as the format spec start. Expressions that
need parentheses in holes: walrus, lambda, and slices.
Implementation - 🔄 Lowered - Hoisted variable declaration with manufactured sinks: