Skip to content

Supporting runtime checked cast functions for generic types #15682

@tilsche

Description

@tilsche

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions