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" ":" suiteunsafe: 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_mismatchtyc::nullable_usetyc::interface_isinstancetyc::arg_counttyc::not_callabletyc::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 intRe-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())def mystery() -> int: return 42
def parse() -> int: if True: v = mystery() 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 uidfrom typhon_runtime.cast import checked_cast as __typhon_checked_cast__
def load(resp: Response) -> dict[str, int]: data = __typhon_checked_cast__(resp.json(), dict[str, int]) return data
def first_id(row: Row) -> int: uid = __typhon_checked_cast__(row[0], int) return uidIt 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
| Target | Check |
|---|---|
int, str, bool, bytes, None, other ordinary classes | isinstance (classes are shallow) |
float, complex | Numeric widening is accepted: integers (and bool) inhabit float targets |
Any, object | Accept 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 |
| Newtypes | The underlying base type |
| Interfaces | Shallow 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 descriptors | Refused |
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
.dtystub — for a long-lived dependency. as!— for an ad-hoc one-off shape assertion. It is the sound, runtime-checked upgrade over a bareunsafe: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
- The Unsafe Boundary — the full reference.
- Wrapping an Untyped Library (recipe) — patterns.