Skip to content

unsafe

unsafe: opens a lexical region where the checker tolerates Any-shaped expressions. Values inside acquire an Unsafe[T] marker that cannot cross out without re-assertion.

Syntax

"unsafe" ":" suite
unsafe:
let data = some_untyped_lib.fetch()
let first = data[0]
let tag = first.get("tag")

What is suppressed

Inside unsafe:, the checker silences these diagnostics:

  • tyc::type_mismatch
  • tyc::nullable_use
  • tyc::interface_isinstance
  • tyc::arg_count
  • tyc::not_callable
  • tyc::non_exhaustive_match

Outside the block, diagnostics apply normally.

Boundary check

Values originating in unsafe: carry a hidden Unsafe[T] marker. They cannot flow into a concrete-typed context without re-assertion:

def parse() -> int:
unsafe:
let v = mystery()
return v # ❌ Unsafe[Any] does not satisfy int

Re-assert via annotation, narrowing, or cast:

def mystery() -> int:
return 42
def parse() -> int:
unsafe:
let v = mystery()
let checked: int = int(v)
return checked
def main() -> None:
print(parse())

Lowering

unsafe: lowers to if True: so scope rules are preserved.

as! — the checked boundary cast

At a single untyped boundary, EXPR as! TYPE is the modern, sound, one-line replacement for the unsafe: block + re-assertion idiom above. The checker types the whole expression as TYPE, so a boundary value (which may be Any) flows in freely — no unsafe: block, no re-assertion, no unsafe_value_leak footgun:

def load(resp: Response) -> dict[str, int]:
let data = resp.json() as! dict[str, int] # was: unsafe: ... then re-assert
return data
def first_id(row: Row) -> int:
let uid = row[0] as! int
return uid

It reuses the same machinery as type[T] generic inference — it is not a bespoke special case.

Sound, not blind

Unlike a static-only re-assertion (which trusts the boundary blindly) or TypeScript’s unchecked as, as! is checked at runtime. The lowering injects from typhon_runtime.cast import checked_cast as __typhon_checked_cast__, and the call __typhon_checked_cast__(EXPR, TYPE) checks the value against TYPE and returns the same object. On a mismatch it raises TypeError, so an as! can only let through values it cannot prove wrong.

Supported targets

TargetCheck
int, str, bool, bytes, None, other ordinary classesisinstance (classes are shallow)
float, complexNumeric widening is accepted: integers (and bool) inhabit float targets
Any, objectAccept any value
Unions and nullable types (int | str, T?)At least one member must match
Literal[...]Exact value and primitive type membership
list[T], set[T], frozenset[T]Container kind and every element recursively
dict[K, V], Mapping[K, V], MutableMapping[K, V]Mapping kind and every key and value recursively
Sequence[T], Collection[T], AbstractSet[T]Collection kind and every element recursively
Fixed tuples and tuple[T, ...]Length and each slot, or every element recursively
Transparent aliases, including generic aliases with concrete arguments (Rows[int])The alias is expanded and its arguments substituted; the underlying check applies
NewtypesThe underlying base type
InterfacesShallow member presence; method bodies and field values are not inspected
Bare type parameters, unbound alias parameters, Callable, iterator / awaitable contracts, parameterised user classes (Box[int]), other unknown descriptorsRefused

A refused target is a check-time error (as! cannot check target `Box[int]` at runtime; choose a concrete supported target, reported as tyc::generic), and the generated runtime refuses it as well. Recursive aliases are supported for finite values; a cyclic value fails the cast. A shallow class or interface cast establishes the runtime class or member surface; it does not inspect the internals of an instance.

Any and object are the only targets that accept everything. A target the runtime cannot check is refused rather than accepted silently.

VM behaviour

The in-process VM intercepts __typhon_checked_cast__ before argument evaluation and runs a structural check of its own, so a wrong-shaped scalar, container, tuple, union or plain alias raises TypeError under tyc run as it does after tyc build. It does not yet enforce Literal, newtype, generic-alias or interface targets: those casts pass under tyc run and raise TypeError on CPython. tyc fmt preserves the surface as! syntax.

Which boundary tool to reach for

  • model X: — for a boundary you validate repeatedly.
  • A .dty stub — for a long-lived dependency.
  • as! — for an ad-hoc one-off shape assertion. It is the sound, runtime-checked upgrade over a bare unsafe: re-assertion.

See The Unsafe Boundary → as! for the full treatment.

When to use

  • Exploring a new dependency.
  • One-off scripts touching untyped code.
  • Genuinely dynamic code (eval, RPC stubs).

For anything long-lived, write a .dty stub instead.

See also