-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
Feature
Provide or allow to implement a function that both casts a value to a given type and does a runtime check, raising a TypeError
if it does not match.
Pitch
Most commonly, type narrowing / type guards are used to combine runtime and static type checking:
assert isinstance(arg, int)
# do int stuff with arg
However, this does not work in many situations when used within expressions here conciseness is wanted. Consider:
process(foo=checked_cast(int, untyped_dict["foo"]), bar=checked_cast(int | str, untyped_dict["bar"]))
We would have to write it as:
foo=untyped_dict["foo"]
assert isinstance(foo, int)
bar=untyped_dict["bar"]
assert isinstance(bar, (int, str))
process(foo=foo, bar=bar)
Now I often find myself implementing
def checked_cast(cls: type[CastT], var: object) -> CastT:
"""
Runtime-check an object for a specific type and return it cast as such
"""
if not isinstance(var, cls):
msg = f"{var}: expected {cls.__name__}, not {var.__class__.__name__}"
raise TypeError(msg)
return var
Unfortunately this does not support anything but basic types, in particular no unions. And I cannot seem to get support for unions.
This seems to be related to the issues #15662, #9003, and #9773.
There is a proposed workaround with a type annotation class: #9773 (comment). However, I cannot get the actual generic type at runtime unless explicit subclasses are created:
class Cast(Generic[CastT]):
@classmethod
def check(cls, value: object) -> CastT:
types = get_args(cls.__orig_bases__[0]) # type: ignore[attr-defined]
assert len(types) == 1
# Handle Union[type, ...] cases
print("Checking for type", types[0])
if hasattr(types[0], "__origin__") and types[0].__origin__ is Union:
types = get_args(cls)
if not isinstance(value, types):
expected = ", ".join(t.__name__ for t in types)
msg = f"{value}: expected {expected}, not {value.__class__.__name__}"
raise TypeError(msg)
return value # type: ignore
# Fails at runtime trying to check the TypeVar "~CastT"
# reveal_type(Cast[int | str].check(1))
class CastInt(Cast[int | str]):
pass
reveal_type(CastInt.check(1)) # works both for static checking and runtime
At this time I am desperate and looking for solutions and viable workarounds here. While there are a number of related issues that when resolved would allow implementing this, I could not find an issue that specifically focuses on this use-case.