I've tried looking for answers to this, as I can't possibly be the first one to stumble across this issue, but my google-fu is failing me terribly.
Is it possible to make mypy
understand that certain functions/methods are only meant to be called from within another method?
Let's take the following code as example (E: for clarity - this is a simplified, runnable version of a real-life problem that involves a Django model instance that has an intentionally nullable field):
from typing import Optional
class Foo:
def __init__(self, value: Optional[int] = None) -> None:
self.value = value
def compose_method(foo: Foo) -> None:
if not foo.value:
raise RuntimeError("Oh no!")
__method_1(foo)
__method_2(foo)
def __method_1(foo: Foo) -> None:
foo.value + 1
def __method_2(foo: Foo) -> None:
foo.value + 2
Running mypy
on this file unsurprisingly results in:
test_mypy.py:18: error: Unsupported operand types for + ("None" and "int")
test_mypy.py:18: note: Left operand is of type "Optional[int]"
test_mypy.py:22: error: Unsupported operand types for + ("None" and "int")
test_mypy.py:22: note: Left operand is of type "Optional[int]"
It's obviously right, but it doesn't take into account the fact that the methods are only intended to be called from within compose_method
- which already has checked that value != None
. I don't want to duplicate code and put the if not foo.value:
condition in each method where foo.value
is used just to satisfy the tooling. At the same time, putting # type: ignore
also doesn't quite sit right with me.
Is there some best practice/way to handle this that the community has agreed is good and right? Or am I just doomed to duplicate the code (and sure, the check can be put in a separate method and called in each of the methods that compose_method
consists of) or use # type: ignore
comments?
You may write things such that __method_1
and __method_2
are only called from within compose_method
but, for all mypy knows, someone will import your file and calls those methods directly. Remember that Python doesn't really have a concept of private items.
What you can do is squash the error by telling mypy, in essence, "I promise that value
isn't None
here," by using typing.cast
.
def __method_1(foo: Foo) -> None:
foo.value = typing.cast(int, foo.value) + 1
There is a slight performance hit here. Since this doesn't use +=
, the Python compiler doesn't emit the INPLACE_ADD
byte code. I originally thought to do instead typing.cast(int, foo.value) += 1
. However, with the function call, it's no longer an L-value and therefore becomes invalid syntax.
Because of that, the best thing to do may just be the # type: ignore
.