Houston4444
Houston4444

Reputation: 1

Python typing: Is there a way to type a variable with the value of another variable

I would like to know if there is the possibility to set a variable type depending on the value of another variable.

I use pyliblo3, it allows to send OSC messages (using UDP, TCP or UNIX socket), when a message is received, we can read the message in a function written this way:

def _message_received(path: str, types: str, args: tuple):
    # Each character of 'types' defines the type of args items
    # for example if types == 'sifb'
    # it means that 'args' is of type tuple[str, int, float, bytes]
    ...

so, of course, I can unpack args this way:

def _message_received(path: str, types: str, args: tuple):
    if types == 'sif':
        args: tuple[str, int, float]
        value_str, value_int, value_f = args

I wonder if it is possible to automatize this process, for example defining a function

def typed_args(args: tuple, types: str):
    ret_args = []

    for i in range(len(types)):
        type_ = types[i]
        arg = args[i]
        if type_ == 's':
            ret_args.append(str(arg))
        elif type_ == 'f':
            ret_args.append(float(arg))
        elif type_ == 'i':
            ret_args.append(int(arg))

    return tuple(ret_args)


def _message_received(path: str, types: str, args: tuple):
    if types == 'sif':
        args = typed_args(args, types)
        # of course here, pylance can't know the type of 'args'.
        # I wonder if there is a way to force it to execute 'typed args'
        # with the fact it knows that types == 'sif'

Can I write a function in order that pylance knows the types of the 'args' variable ? All I can know for the moment is that args is of type tuple[str | int | float | bytes], but pylance can't know nor its length, nor the types of its tuple items. It would be convenient when unpacking them.

Upvotes: 0

Views: 76

Answers (1)

Dunes
Dunes

Reputation: 40833

You can write an overloaded TypeIs guard, but it is a lot of extra code. There will be one overload for each distinct type format. So if this is a frequently used idiom in the code it might make sense. And if there is ever a bug in the code, the guard can be configured to be strict about checking types, and not just trusting the type format.

Example

from typing import overload, reveal_type, Any, Literal
from typing_extensions import TypeIs

RUNTIME_TYPE_CHECKING = True

@overload
def is_type(obj: Any, types: str, fmt: Literal['i']) -> TypeIs[int]: ...
@overload
def is_type(obj: Any, types: str, fmt: Literal['f']) -> TypeIs[float]: ...
@overload
def is_type(obj: Any, types: str, fmt: Literal['s']) -> TypeIs[str]: ...
@overload
def is_type(
    obj: Any, types: str, fmt: Literal['ifs']
) -> TypeIs[tuple[int, float, str]]: ...
def is_type[T](obj: Any, types: str, fmt: str) -> TypeIs[T]:
    if not RUNTIME_TYPE_CHECKING:
        return types == fmt

    match fmt:
        case 'i':
            return isinstance(obj, int)
        case 'f':
            return isinstance(obj, float)
        case 's':
            return isinstance(obj, str)
        case _:
            if isinstance(obj, tuple) and len(fmt) == len(obj):
                for item, char_types, char_fmt in zip(obj, types, fmt):
                    # char_fmt is not a literal, so type checkers will not like
                    # thiw following check. However, it will work as expected
                    # when strictly checking types. So just tell type checker
                    # to ignore this line.
                    if not is_type(item, char_types, char_fmt): # type: ignore
                        return False
                return True
            else:
                return False

def get_data() -> tuple[tuple[int | float | str, ...], str]:
    return (1, 2.5, 'string'), 'ifs'

def main() -> None:
    obj, types = get_data()
    reveal_type(obj) # Revealed type is "tuple[Union[int, float, str], ...]"

    # this guard will narrow the type according to the type format
    if is_type(obj, types, 'ifs'):
        x, y, z = obj
        reveal_type(obj) # Revealed type is "tuple[int, float, str]"
        reveal_type(x)   # Revealed type is "int"
        reveal_type(y)   # Revealed type is "float"
        reveal_type(z)   # Revealed type is "str"
        print(obj)
    
    # after guard, the type widens to its previous value
    reveal_type(obj) # Revealed type is "tuple[Union[int, float, str], ...]"

Upvotes: 0

Related Questions