How to handle Unsupported operand types in mypy when type should be clear?

Question:

Consider the following toy example.

class MyClass:
    def __init__(self, optional_arg: float | None = None) -> None:
        self.optional_arg = optional_arg

        self._parse_args()

    def _parse_args(self) -> None:
        if self.optional_arg is None:
            self.optional_arg = 0.5

    def multiply(self) -> float:
        # mypy is complaining with the below statement:
        # error: Unsupported operand types for * ("float" and "None")  [operator]
        # Right operand is of type "Optional[float]"
        return 0.5 * self.optional_arg

After instantiating this class with optional_arg=None, it should be clear that self.optional_arg will be set to a float due to the _parse_args method.

In my view, it should be clear that when calling the method multiply, it will return a float. However, mypy is still complaining that self.optional_arg might be None.

What is a pythonic way to tell mypy that self.optional_arg can’t possibly be None?

Asked By: Andi

||

Answers:

self.optional_arg doesn’t have to be the value set by _parse_args. Someone might do

x = MyClass()
x.optional_arg = None
x.multiply()

and as far as mypy is concerned, that’s a valid way to use MyClass.

Alternatively, someone might override _parse_args:

class Subclass(MyClass):
    def _parse_args(self) -> None:
        pass

Subclass().multiply()

and as far as mypy is concerned, that’s legal too.

If you want mypy to treat self.optional_arg as never None, you shouldn’t set it to a value that could be None in the first place. You can use a float as the default in the first place:

class MyClass:
    optional_arg: float
    def __init__(self, optional_arg: float = 0.5) -> None:
        self.optional_arg = optional_arg
    ...

or do your None handling before you set the attribute:

class MyClass:
    optional_arg: float
    def __init__(self, optional_arg: float | None = None) -> None:
        self.optional_arg = optional_arg if optional_arg is not None else 0.5
    ...
Answered By: user2357112

user2357112 answer is great and you should go for it in that case.
However not everytime it’s easy to have proper typing (excecially working with legacy code). In such case you can use type narrowing.

     def multiply(self) -> float:
        assert isinstance(self.optional_arg, float)
        return 0.5 * self.optional_arg
Answered By: kosciej16
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.