Skip to content

Operators and Precedence

Typhon’s operators are Python’s, plus three:

  • |> — pipe, for left-to-right composition.
  • ? — nullable type suffix, and Result-propagation expression suffix (context-dependent).
  • as! — the checked boundary cast: EXPR as! TYPE (see below).

Precedence table

From tightest to loosest binding. Within a row, operators are left-associative unless noted otherwise.

PrecedenceOperatorsTypeNotes
1 (tightest)f(...), x[...], x.attrcall, subscript, attribute
2**exponentright-associative
3+x, -x, ~xunary
4*, @, /, //, %multiplicative
5+, -additive
6<<, >>bitwise shift
7&bitwise AND
8^bitwise XOR
9|bitwise OR
10|>pipe (Typhon-specific)
11in, not in, is, is not, <, <=, >, >=, !=, ==comparisonchainable: 0 < x < 10
12not xboolean NOT
13andboolean ANDshort-circuit
14orboolean ORshort-circuit
15a if c else bconditional
16lambdaanonymous fn
17:=walrus
18 (loosest)assignment, yield, returnstatements

The ? suffix on a Result-typed expression binds at the same precedence as f(...) (call) — it’s part of the postfix chain.

Pipe precedence in detail

A |> chain must end its syntactic slot: arithmetic may feed into the pipe on the left, but no operator may follow the chain — comparisons, boolean ops, arithmetic tails and ternaries after a pipe are all parse errors. Parenthesise the chain to use its value further:

x + 1 |> f() # ✅ == f(x + 1): arithmetic feeds the pipe
x |> f() |> g() # ✅ == g(f(x)): chains nest left
(x |> f()) == 0 # ✅ parenthesised: compare the piped value
x |> f() == 0 # ❌ parse error — parenthesise as above
x |> f() and g(x) # ❌ parse error — parenthesise as above

When in doubt, parenthesise.

Augmented assignments

x += 1
x -= 1
x *= 2
x /= 2.0
x //= 2
x %= 2
x **= 2
x <<= 1
x >>= 1
x &= 0xFF
x |= 0x01
x ^= 0xAA

All require the target to be mut. The checker enforces.

The as! checked cast

EXPR as! TYPE is the checked boundary cast. Unlike a normal infix operator it does not slot into the precedence table — it is rewritten structurally rather than parsed by precedence:

  • Left operand — the whole current syntactic slot: everything back to the enclosing bracket, a top-level , / ; / : separator, an assignment / augmented / walrus =, a return / yield / if / while / assert keyword, or the line start. So a + b as! int casts a + b, and save(row[0] as! int, label) casts only row[0].
  • Right operand — a type expression: a dotted name, an optional [...] subscript, and |-unions. Trailing code after the type stays outside the cast — x as! int + 1 casts x then adds 1, and the for of a comprehension ([x as! int for x in xs]) stays outside.

An as! whose right side isn’t a type expression is left for the parser to reject cleanly. It lowers to __typhon_checked_cast__(EXPR, TYPE), a recursively-checked runtime cast. See as!.

Type-only operators

  • T? — nullable suffix on a type expression (only in annotation position).
  • T | U — union (annotation position); T or U is not valid in annotations.

Walrus (:=)

Standard Python:

if (n := len(xs)) > 10:
print(f"{n} is too many")

The walrus binds — and the binding is implicitly let (no mut form supported).

Common gotchas

is vs ==

Same as Python: is is identity, == is equality. Use is None / is not None for nullability checks (the checker only narrows on is).

or for nullable defaults

let name: str = maybe_name or "anonymous"

or collapses falsy values (including None) to the right side. Common idiom for narrowing T? to T when “empty” is treated like “missing”.

Pipe and method calls

"hello" |> str.upper() # str.upper("hello") → "HELLO"
"hello".upper() # also "HELLO"

Both work. Use whichever reads more naturally.

Where next