Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting runtime checked cast functions for generic types #15682

Closed
tilsche opened this issue Jul 15, 2023 · 6 comments
Closed

Supporting runtime checked cast functions for generic types #15682

tilsche opened this issue Jul 15, 2023 · 6 comments
Labels

Comments

@tilsche
Copy link

tilsche commented Jul 15, 2023

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.

@hauntsaninja
Copy link
Collaborator

If you're looking at that comment of mine #9773 (comment) and are are actually desperate, you can combine it with an if TYPE_CHECKING hack to make it work at runtime as well. That is, put the workaround from my comment under if TYPE_CHECKING and in the else clause do whatever runtime shenanigans you need to persist the type information.

@tilsche
Copy link
Author

tilsche commented Jul 15, 2023

If you're looking at that comment of mine #9773 (comment) and are are actually desperate, you can combine it with an if TYPE_CHECKING hack to make it work at runtime as well. That is, put the workaround from my comment under if TYPE_CHECKING and in the else clause do whatever runtime shenanigans you need to persist the type information.

Interesting, yes that might actually work.

However you do still end up end up with Check[MyType].cast(value) as syntax, right?

@erictraut
Copy link

Is there anything actionable for mypy to implement, or are you requesting some addition to the Python type system? If it's the latter, then the python/typing discussion forum would be a better place for the request.

@AlexWaygood
Copy link
Member

Agreed, there's nothing actionable for mypy here

@AlexWaygood AlexWaygood closed this as not planned Won't fix, can't repro, duplicate, stale Aug 25, 2023
@tilsche
Copy link
Author

tilsche commented Aug 28, 2023

Is there anything actionable for mypy to implement, or are you requesting some addition to the Python type system?

I wanted to keep the issue description broad to avoid an X-Y problem. There are several linked mypy-specific issues which may help towards my formulated goal.

I also thought that other developers have the same issue and this could make it worthwhile to eventually include a idiomatic solution in the mypy documentation along the type system reference.

But I don't mind closing this for now. Developers with the same issue can still contribute here and if a idiomatic solution becomes nearer we can go there...

@travisdowns
Copy link

@tilsche did you end up opening a discussion on this somewhere else? I'm also interested in a better way to accomplish this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants