2

I'm trying to use typing with a function that has conditional parameters, that works like this:

from typing import Optional, Union


class Foo:
    some_param_to_check: str = 'foo_name'
    one_param_exclusive_to_foo: int


class Bar:
    some_param_to_check: str = 'bar_name'
    another_param_exclusive_to_bar: str


def some_process_that_returns_a_bool(
    f_or_b: Union[Foo, Bar],
    a_name: str,
) -> bool:
    return f_or_b.some_param_to_check == a_name


def do_something_with_foo_or_bar(
    foo: Optional[Foo],
    bar: Optional[Bar],
    some_name: str,
) -> bool:
    if not foo and not bar:
        raise ValueError('You need to specify either "foo" or "bar".')

    # I added this explicit type hint after the first error, hoping it would solve the issue:
    foo_or_bar: Union[Foo, Bar]  # later becomes Union[Foo, Bar, None]
    foo_or_bar = foo if foo else bar

    return some_process_that_returns_a_bool(foo_or_bar, some_name)


foo_obj = Foo()
bar_obj = Bar()

# This will work:
do_something_with_foo_or_bar(foo_obj, bar_obj, 'test_string')

# This will also work:
do_something_with_foo_or_bar(foo_obj, None, 'test_string')

# This too:
do_something_with_foo_or_bar(None, bar_obj, 'test_string')

# But this should not:
do_something_with_foo_or_bar(None, None, 'test_string')

To add more context:

The function works by expecting foo, or, if not available, bar. If foo is not None, bar will essentially be ignored.

When checking with mypy, it complains about:

Incompatible types in assignment (expression has type "Union[Foo, Bar, None]", variable has type "Union[Foo, Bar]"

(I'm guessing because of the Optional in the parameter type hints.)

If I then add None as the type hint for foo_or_bar then the error becomes:

error: Item "None" of "Union[Foo, Bar, None]" has no attribute "some_param_to_check"

How would I fix this so that mypy stops complaining (while still keeping the type hints)?

0

3 Answers 3

3

I'm pretty sure this is just a mypy issue. Its type inference system is not clever enough to recognize that your if not foo and not bar block that raises an exception excludes the double None case later on (since it can't conclusively infer anything about either type in isolation). There doesn't seem to be a good way to directly fix the type hinting, but you could instead change the logic a bit to separate the cases more clearly, and mypy should understand it better:

def do_something_with_foo_or_bar(
    foo: Optional[Foo],
    bar: Optional[Bar],
    some_name: str,
) -> bool:
    foo_or_bar: Union[Foo, Bar]
    if foo:
        foo_or_bar = foo
    elif bar:
        foo_or_bar = bar
    else:
        raise ValueError('You need to specify either "foo" or "bar".')

    return some_process_that_returns_a_bool(foo_or_bar, some_name)

You could also get rid of the foo_or_bar variable all together and just put two different function calls in the if and elif blocks, using foo or bar as appropriate.

Sign up to request clarification or add additional context in comments.

Comments

1

You're getting the error because in some_process_that_returns_a_bool you are trying to access some_param_to_check on a value that can be None and None does not have that property.

If some_process_that_returns_a_bool should accept None as a possible value for f_or_b , then you should check for None before trying to access any properties.

def some_process_that_returns_a_bool(
    f_or_b: Union[Foo, Bar, None],
    a_name: str,
) -> bool:
    if f_or_b is None:
      # Handle None...
      return False
    else:
      return f_or_b.some_param_to_check == a_name

This way you will only try to access some_param_to_check when f_or_b is not None and else you will return False when f_or_b is None.

But please handle None in whatever way makes sense for your application.

3 Comments

Union[Optional[Foo], Optional[Bar] is a confusing way of writing Union[Foo, Bar, None]
Anyway, this is one way to handle it, although, the OP doesn't indicate what should happen if the argument is None.
@juanpa.arrivillaga You're right. The intended point with the example was only that a None check should be implemented if None is an intended possible value for the argument. I will edit the text to reflect that.
1

Adding my $0.02 to @Blckknght answer:

This solves the problem with inner type checking (no errors inside function body, if you fix a missing annotation), but doesn't help external callers.

To achieve typing "one or another, but never none of them", you can use overloads:

from typing import Optional, Union, overload


class Foo:
    some_param_to_check: str = 'foo_name'
    one_param_exclusive_to_foo: int


class Bar:
    some_param_to_check: str = 'bar_name'
    another_param_exclusive_to_bar: str


def some_process_that_returns_a_bool(
    f_or_b: Union[Foo, Bar],
    a_name: str,
) -> bool:
    return f_or_b.some_param_to_check == a_name

@overload
def do_something_with_foo_or_bar(
    foo: Foo,
    bar: Optional[Bar],
    some_name: str,
) -> bool: ...
@overload
def do_something_with_foo_or_bar(
    foo: Optional[Foo],
    bar: Bar,
    some_name: str,
) -> bool: ...
def do_something_with_foo_or_bar(
    foo: Optional[Foo],
    bar: Optional[Bar],
    some_name: str,
) -> bool:
    foo_or_bar: Union[Foo, Bar]  # This annotation was missing
    if foo:
        foo_or_bar = foo
    elif bar:
        foo_or_bar = bar
    else:
        raise ValueError('You need to specify either "foo" or "bar".')

    return some_process_that_returns_a_bool(foo_or_bar, some_name)
    

do_something_with_foo_or_bar(Foo(), Bar(), '')
do_something_with_foo_or_bar(None, Bar(), '')
do_something_with_foo_or_bar(Foo(), None, '')
do_something_with_foo_or_bar(None, None, '')  # Line 51

And voila:

main.py:51: error: No overload variant of "do_something_with_foo_or_bar" matches argument types "None", "None", "str"  [call-overload]
main.py:51: note: Possible overload variants:
main.py:51: note:     def do_something_with_foo_or_bar(foo: Foo, bar: Optional[Bar], some_name: str) -> bool
main.py:51: note:     def do_something_with_foo_or_bar(foo: Optional[Foo], bar: Bar, some_name: str) -> bool

Here's a playground link.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.