Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mypy 0.6.4 return type Optional[str] but sometimes you have prior knowledge about the type you will get

I have a function that is returning either an class instance or None depending on some logic. In some places of the code I know this function is for sure not returning None, but mypy complains.

I made a minimal example that reproduces the situation described above.

I would like to avoid marking a_string as a_string: Optional[str] = "", I know I can also overcome the problem using cast or type ignore, but somehow I feel there might be a better way.

Any recommendations how to handle this situation?

For this example I am using mypy 0.641 and python 3.7

"""
Function returns either an object or none

"""

from typing import Optional, cast

RET_NONE = False


def minimal_example() -> Optional[str]:
    if RET_NONE:
        return None
    else:
        return "my string"


a_string = ""
maybe_string = minimal_example()
a_string = maybe_string

# mypy doesn't complain if I do the following
a_string = cast(str, maybe_string)
a_string = maybe_string  # type: ignore

Mypy complains as follows:

❯❯❯   mypy mypy_none_or_object.py                                                                                                                                                                         (chatsalot)  ✘ 1
mypy_none_or_object.py:19: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str")
like image 910
Cesc Avatar asked Nov 13 '18 09:11

Cesc


People also ask

How do I ignore MYPY errors?

You can use a special comment # type: ignore[code, ...] to only ignore errors with a specific error code (or codes) on a particular line. This can be used even if you have not configured mypy to show error codes. Currently it's only possible to disable arbitrary error codes on individual lines using this comment.

What is MYPY?

“Mypy is an optional static type checker for Python that aims to combine the benefits of dynamic (or 'duck') typing and static typing. Mypy combines the expressive power and convenience of Python with a powerful type system and compile-time type checking.” A little background on the Mypy project.


2 Answers

Mypy is designed to treat function signatures as the "source of truth". If you indicate that some function returns an Optional[str], then mypy will assume that will always be the case. It won't attempt to see how any global variables may or may not alter that function signature.

The easiest possible way of working around this is to add an assert or isinstance check:

maybe_string = minimal_example()
reveal_type(maybe_string)           # Revealed type is Optional[str]
assert maybe_string is not None     # Or use 'if isinstance(maybe_string, str)
reveal_type(maybe_string)           # Revealed type is str

(If you're not aware, mypy will special-case the reveal_type(...) function: whenever mypy encounters it, mypy prints out the type of whatever expression you provide. This is useful for debugging, but you should remember to delete the pseudo-function after you're done since it doesn't exist at runtime.)

Alternatively, you could redesign your code so that your function's return value is more normalized -- it always returns a string instead of sometimes returning one.

If RET_NONE is meant to be a more-or-less immutable global (e.g. something like "enable debug mode" or "assume we're running on Windows"), you could use is to take advantage of mypy's --always-true and --always-false flags and provide two different definitions of minimal_example. For example:

RET_NONE = False

if RET_NONE:
    def minimal_example() -> None:
        return None
else:
    def minimal_example() -> str:
        return str

You then invoke mypy using mypy --always-true RET_NONE or mypy --always-false RET_NONE to match how your variable is defined. You can find more info about these types here and maybe here.

A fourth alternative you could explore is using function overloads: https://mypy.readthedocs.io/en/latest/more_types.html#function-overloading

However, idk if that really works in your case: you can't define overloads where only the return type differs: the argument arity or types of each overload need to be distinguishable from each other in some way.

like image 139
Michael0x2a Avatar answered Dec 09 '22 07:12

Michael0x2a


Both solutions: cast() and # type: ignore are effectively turning off mypy checks for the variable. This can shadow bugs and should be avoided when possible.

In your case mypy cannot know the value of RET_NONE, since it can be changed in runtime from False to anything else, thus the error.

I suggest adding an assertion:

a_string = ""
maybe_string = minimal_example()
assert maybe_string is not None   # <- here
a_string = maybe_string

Now mypy is sure that on the next line maybe_string definitely won't be None. I covered this in the Constraining types section of my blog post about typing.

like image 25
pawelswiecki Avatar answered Dec 09 '22 08:12

pawelswiecki