checked cast¶
the plain cast only widens. for a downcast — taking an object as
an int — basedpython has two operators that check the value at runtime and
differ in what they do when it does not match. cast! is checked: it
raises. cast? is safe: it yields None.
cast! — checked (raises)¶
<value> cast! <type> narrows the value to the target type and verifies it at
runtime:
def f(a: object):
b = a cast! int
print(b)
f(1) # prints 1
f("x") # raises TypeError: cast to int failed: value is str
transpiles to:
def _checked_cast(_v, _t):
if not isinstance(_v, _t):
raise TypeError(
f"cast to {getattr(_t, '__name__', _t)} failed: value is {type(_v).__name__}"
)
return _v
def f(a: object):
b = _checked_cast(a, int)
print(b)
its type is the target type (b is int).
cast? — safe (returns None)¶
<value> cast? <type> yields the value when it matches and None otherwise,
so its type is <type> | None:
def f(a: object):
b = a cast? int
reveal_type(b) # revealed: int | None
f(1) # b is 1
f("x") # b is None
transpiles to b = _try_cast(a, int), with the helper:
shared rules¶
both forms:
- check as deeply as the target allows at runtime:
- a user generic is checked in full. its instances carry
__orig_class__(stamped byA[int](…)), sox cast! A[int]rejects anA[str], respecting each type parameter's variance. a value carrying no reification passes the argument check — there is nothing to compare — leaving the base class as the guarantee - anything else collapses to its runtime origin:
a cast! list[int]checksisinstance(a, list), because a builtin erases its type arguments andisinstance(a, list[int])is itself aTypeError. the dropped[int]claim is reported by theerased-cast-argumentwarning. a union checks each arm's origin (a cast? list[int] | None→isinstance(a, (list, type(None))))
- a user generic is checked in full. its instances carry
- skip the check entirely when it is provably redundant. if the value is already
the target, the probe would always pass, so the cast degrades to the same
plain
typing.castthe unsuffixedcastemits - evaluate the value exactly once, even when it has side effects
(
g() cast! intcallsg()once) - are basedpython-only: a
.pyfile using either produces a parse error. they never collide with a plaincastidentifier — the suffix follows the keyword directly, and no expression can begin with!or?