Pythonic way to handle arguments with values in Union

Viewed 138

In the code below, print_pos accepts one argument which can be of three different types.

from typing import List, Tuple, Union

pos_t = Tuple[int, int]
anchor_t = Tuple[str, str]
anchor_pos_t = Tuple[anchor_t, pos_t]

def print_pos(
        pos: Union[
            pos_t,
            anchor_pos_t,
            List[Union[pos_t, anchor_pos_t]]
        ]
) -> None:
    if isinstance(pos, tuple) and isinstance(pos[0], int):
        print('xy =', pos)
    elif isinstance(pos, tuple) and isinstance(pos[0], tuple):
        print('anchor =', pos[0])
        print('xy =', pos[1])
    elif isinstance(pos, list):
        print('[')
        for p in pos:
            print_pos(p)
        print(']')
    else:
        raise ValueError('invalid pos')

print_pos((0, 100))
print_pos((('right', 'bottom'), (0, 100)))
print_pos([
    (0, 100),
    (('right', 'bottom'), (0, 100))
])

Right now, I use isinstance to check for the different possibilities for the type of pos but I find the code rather clumsy. Is there a more convenient/elegant way to do that? In particular is there a mean to reuse types I defined (pos_t, anchor_t, anchor_pos_t) in my type check?

2 Answers

You can use the typeguard library to check variable types at runtime.

This library is mainly for runtime type validation, rather than conditional type checking, so an extra is_type function needs to be defined to fit your needs.

Extra type casts are also unfortunately necessary to prevent type checker errors.

from typing import Any, List, Tuple, Union, cast

from typeguard import check_type


pos_t = Tuple[int, int]
anchor_t = Tuple[str, str]
anchor_pos_t = Tuple[anchor_t, pos_t]


def is_type(value: Any, expected_type: Any) -> bool:
    """
    Return whether the given value is of the expected type or not.
    """
    try:
        check_type('<blank>', value, expected_type)
        return True
    except TypeError:
        return False


def print_pos(
    pos: Union[pos_t, anchor_pos_t, List[Union[pos_t, anchor_pos_t]]]
) -> None:
    if is_type(pos, pos_t):
        pos = cast(pos_t, pos)
        print('xy =', pos)
    elif is_type(pos, anchor_pos_t):
        pos = cast(anchor_pos_t, pos)
        print('anchor =', pos[0])
        print('xy =', pos[1])
    elif is_type(pos, List[Union[pos_t, anchor_pos_t]]):
        pos = cast(List[Union[pos_t, anchor_pos_t]], pos)
        print('[')
        for p in pos:
            print_pos(p)
        print(']')
    else:
        raise ValueError('invalid pos')


print_pos((0, 100))
print_pos((('right', 'bottom'), (0, 100)))
print_pos([(0, 100), (('right', 'bottom'), (0, 100))])

This isn't the cleanest solution, but it works.

I would recommend using a more object-oriented approach with classes if possible, to eliminate the need for union types.

This does not yet work with any current python version, but python 3.10 (planned to be released October 2021) will have structural pattern matching.

This allows something like this, which might be slightly more readable:

from typing import List, Tuple, Union

pos_t = Tuple[int, int]
anchor_t = Tuple[str, str]
anchor_pos_t = Tuple[anchor_t, pos_t]

def print_pos(
        pos: Union[
            pos_t,
            anchor_pos_t,
            List[Union[pos_t, anchor_pos_t]]
        ]
) -> None:
    match pos:
        # need to match this more specific case first, as tuple((x, y)) matches this as well
        case tuple(((a, b), (x, y))):
            print('anchor =', (a, b))
            print('xy =', (x, y))
        case tuple((x, y)):
            print('xy =', (x, y))
        case list(_):
            print('[')
            for p in pos:
                print_pos(p)
            print(']')
        case _:
            raise ValueError('invalid pos')

print_pos((0, 100))
print_pos((('right', 'bottom'), (0, 100)))
print_pos([
    (0, 100),
    (('right', 'bottom'), (0, 100))
])

This already works with the 3.10.0a7 pre-release, although the mypy support is not there yet.

Structural pattern matching is in a sense similar to sequence unpacking like

(a, b), (x, y) = pos

but more powerful.

The three PEPs describing structural pattern matching:

Related