Top Pitfalls
The list of mistakes everyone hits at least once. Internalising these will save you a lot of red squiggles.
1. Forgetting -> None
Sync functions returning nothing still need the annotation.
def main(): # ❌ print("hi")
def main() -> None: # ✅ print("hi")2. Writing x = 1 at function scope
Locals require let or mut.
def f() -> None: x = 1 # ❌ let x: int = 1 # ✅Module-level bindings default to let.
3. Passing dict.get(k) somewhere expecting V
dict.get(k) returns V?. Narrow first.
let counts: dict[str, int] = {"a": 1}let n: int = counts.get("missing") # ❌let n: int = counts.get("missing", 0) # ✅ (default)4. Putting def display(self) -> str inside class
Methods go in impl blocks.
class User: id: int
def display(self) -> str: # ❌ ...
# ✅class User: id: int
impl User: def display(self) -> str: return f"user {self.id}"5. Writing __init__
Don’t. The constructor is generated. Use field defaults or a free function.
class User: id: int
def __init__(self, id: int) -> None: ... # ❌For framework bases that need __init__, use class!.
6. from typing import TypeVar
Use PEP 695 instead.
from typing import TypeVarT = TypeVar("T") # ❌
def first[T](xs: list[T]) -> T?: ... # ✅7. isinstance(x, MyInterface)
Rejected by default — PEP 544 only checks attribute presence, not signatures. Use static narrowing or refactor to a sealed union.
8. asyncio.create_task(...) for fire-and-forget
Use go f(x) instead — the runtime registry holds a strong reference.
9. lazy from numpy import array
Rejected at parse time. Use lazy import np = numpy plus np.array(...).
10. comptime let NOW: float = time.time()
The sandbox forbids time.*. Compute at runtime with lazy let or a plain function.
11. Returning early from a with-chain without an else err:
Fine — but only if the enclosing function returns a compatible Result. Without an else, the first Err short-circuits via the function’s return.
12. Empty container without annotation
let xs: list = [] # ❌ implicit Any elementlet xs: list[int] = [] # ✅13. Putting blocking I/O inside async def
Typhon doesn’t catch this (it’s a Python-wide hazard). Use aiofiles or asyncio.to_thread(...).
14. Treating bool as int
The checker treats them as distinct. Cast explicitly with int(b) / bool(n).
15. Mutating a frozen instance
class P frozen: x: float
let p = P(x=1.0)p.x = 2.0 # ❌Construct a new instance.
16. class X(Base) frozen: ordering
The frozen modifier comes between the class name and the base list, not after it:
class Square(Shape) frozen: # ❌ does not parse side: float
class Square frozen(Shape): # ✅ side: floatThe same applies to generics — type parameters sit before frozen: class Stack[T] frozen:, class Stack[T] frozen(BaseStack):.
17. Unannotated *args / **kwargs
Rule 1 (every parameter annotated) extends to variadic parameters since v0.9.0. Canonical idiom for genuinely variadic functions is object:
def trace(f, *args, **kwargs): # ❌ tyc::missing_annotation on each ...
def trace[R](f: Callable[..., R], *args: object, **kwargs: object) -> R: # ✅ return f(*args, **kwargs)18. func[T](args) for explicit type instantiation
Rejected at check time since v0.9.0. Let the binding type drive inference instead:
let p = pair[int](1, 2) # ❌ tyc::operator_type_mismatchlet p: tuple[int, int] = pair(1, 2) # ✅ T inferred from the bindinglet empty: list[int] = first([]) # ✅ for inference-failing positions19. ? inside a comprehension
Comprehensions lower to nested loops in Python, so the surrounding function frame ? would short-circuit out of is not the comprehension’s frame:
def collect(xs: list[str]) -> Result[list[int], str]: return Ok([parse(x)? for x in xs]) # ❌ tyc::invalid_question_opPre-extract with an explicit loop, or chain .and_then / .map:
def collect(xs: list[str]) -> Result[list[int], str]: mut out: list[int] = [] for x in xs: out.append(parse(x)?) return Ok(out)20. freeze let X = SomeMutableClass()
The check pass validates the RHS of every freeze let since v0.9.0. Constructing a non-frozen user class on the RHS now fires tyc::freeze_not_freezable at check time instead of failing at first import:
class Counter: # not frozen value: int
freeze let CFG = Counter(value=0) # ❌ tyc::freeze_not_freezableMark the class frozen, switch to a built-in container (list / dict / set wrap automatically), or use a plain let.
21. Heterogeneous variant inside a generic sealed union
Cons[T] flowing into LL[T] works since v0.9.0:
type LL[T] = Cons[T] | Nil[T]
def length[T](xs: LL[T]) -> int: mut cur: LL[T] = xs while True: match cur: case Nil(): return 0 case Cons(_, tail): cur = tail # ✅ since v0.9.0 — was type_mismatch beforeBefore v0.9.0 the assignment fired tyc::type_mismatch because the variant wasn’t recognised as assignable to the parametric alias. The fix is automatic — rebuild against tyc ≥ v0.9.0.
Where next
- Python Habits to Drop — patterns that work in Python but fight Typhon.
- Performance Gotchas — what to watch when perf matters.