5

I'm using type annotatiots to do some basic type checking and automated type conversions.

As part of that, I'm looking to check if a given type should be assignable to some field of a data class, i.e. if the type is "compatible" with the type annotation on my data class.

Is there any out-of-the-box way to check if a given type is assignable to some other type? I.e.

from typing import Optional, Union, Annotated, Type

def is_assignable(from_what: Type, to_what: Type) -> bool:
  // .. how?

is_assignable(int, int) // should be true
is_assignable(int, Optional[int]) should be true
is_assignable(int, Annotated[Union[str, Optional[int]], "hello world"]) // should be true
is_assignable(float, Annotated[Union[str, Optional[int]], "hello world"]) // False. Can't assign float to Annotated[Union[str, Optional[int]], "hello world"])

I could try unwrapping all the typing.* logic myself, i.e. unwrap Optional, Union, Annotated, and so forth, but that feels rather ugly (and not too maintainable, if new stuff is added in more recent python versions).

Bogey
  • 4,926
  • 4
  • 32
  • 57
  • 4
    Why not just using [mypy](https://mypy.readthedocs.io/en/stable/)? – maxi.marufo Mar 15 '21 at 14:11
  • 3
    @maxi.marufo Because I'm not primarily using this to do static type checking, but also e.g. dynamic type conversion. Example: Input is some dataframe that has a pandas.TimeStamp column, but dataclass with matching property name expects a datetime.datetime, then I'm doing the conversion under the hood – Bogey Mar 15 '21 at 15:00
  • Should it be super generic? or will only get builtins for the `from_what` parameter? In other words - would you want to support this call? `is_assignable(Annotated[Union[str], Optional[int]], Optional[int])` - what would that return? what would your last example return if we switch the `float` with `int` and the `int` with `float`? (do you consider that every float can be an "integer" as well?) – CodeCop Feb 10 '23 at 15:04
  • 2
    I have found https://pypi.org/project/typing-utils/#issubtype from this answer: https://stackoverflow.com/questions/68934308/check-parameter-types-dynamically-in-python-and-typings . Maybe this question should be considered a duplicate. However that library hasn't been maintained in a while. – Alexander Kondratskiy Feb 10 '23 at 15:05
  • 2
    @no_hex Ideally, yes, should be super generic. Regarding implicit conversions? Personally I don't care. Perhaps Bogey does – Alexander Kondratskiy Feb 10 '23 at 15:06
  • Another library I found: https://github.com/erezsh/runtype – Alexander Kondratskiy Feb 10 '23 at 18:01

2 Answers2

2

Using pydantic models instead of dataclasses is a way to get this out of the box, for example:

from pydantic import BaseModel, ValidationError

class MyModel(BaseModel):
    a: int
    b: str

try:
    x = MyModel(a=2, b="asd")  # OK
except ValidationError as exc:
    ....  # Catch the error if any, in this case this block will not execute

try:
    x = MyModel(a="asd", b=2)  # Wrong types, cannot parse "asd" to int
except ValidationError as exc:
    ....  # Catch the error and work around it.

Note that types are not strictly enforced, Pydantic is actually a parsing library and not a validation library (although the words validation, validator, etc, are everywhere, in fact the meaning is of parsing). Therefore, MyModel(a=2, b=3) will also work because 3 can be parsed to a string str(3) -> "3", but you can implement your own more strict validators if you want to.

danielcaballero88
  • 343
  • 1
  • 2
  • 10
0

Sorry for my fault. Now it's working as you expected, for me.

from typing import Optional, Union, Annotated, Type, get_origin, get_args

def is_assignable(from_what: Type, to_what: Type) -> bool:
    # Unwrap Annotated type if present
    if get_origin(to_what) is Annotated:
        to_what = get_args(to_what)[0]

    return issubclass(from_what, to_what)

is_assignable(int, int)  # True
is_assignable(int, Optional[int])  # True
is_assignable(int, Annotated[Union[str, Optional[int]], "hello world"])  # True
is_assignable(float, Annotated[Union[str, Optional[int]], "hello world"])  # False