I have a mutable object which I populate with data. Once all the mandatory data is present, the object can be “committed”. Attempting to commit an incomplete object raises an exception. Here's a toy example with an object whose content
is initially None
, and must be populated with a string before committing.
# inline.py
from typing import Optional
class IncompleteFoo(Exception):
pass
class Foo:
def __init__(self) -> None:
self.content = None #type: Optional[str]
def commit(self) -> str:
if self.content is None:
raise IncompleteFoo
return self.content
This code isn't well-structured: there should be a separate method to check completeness. (In my real code, that method would be called in multiple places, because there are several different way to “commit”.)
# check.py:14: error: Incompatible return value type (got "Optional[str]", expected "str")
from typing import Optional
class IncompleteFoo(Exception):
pass
class Foo:
def __init__(self) -> None:
self.content = None #type: Optional[str]
def check_completeness(self) -> None:
if self.content is None:
raise IncompleteFoo
def commit(self) -> str:
self.check_completeness()
return self.content
I use mypy 0.780 to check types. Understandably, it complains about the code above:
check.py:15: error: Incompatible return value type (got "Optional[str]", expected "str")
That's fair: in the first “inline” version, mypy is smart enough to know that self.content
has the type str
given that it has the type Optional[str]
and that this part of the code is only reachable if self.content is None
is false. In the version with a separete check_completeness
method, mypy does not infer that a postcondition of that method is that self.content
is not None
.
How can I let mypy know that a postcondition of check_completeness
is self.content is not None
or self.content : str
? To preserve the encapsulation of the completeness check (which is a lot larger in my real code), I don't want to repeat the condition inside commit
. I wouldd prefer to keep commit
unmodified from the second version above. I could settle for repeating:
# assert.py
from typing import Optional
class IncompleteFoo(Exception):
pass
class Foo:
def __init__(self) -> None:
self.content = None #type: Optional[str]
def check_completeness(self) -> None:
if self.content is None:
raise IncompleteFoo
def is_complete(self) -> bool:
return self.content is not None
def commit(self) -> str:
self.check_completeness()
assert self.is_complete()
return self.content
But that doesn't help: mypy doesn't expand the method call to deduce postconditions for the assert
call.
You have to use
typing.cast
to tellmypy
that yes, this value that might have beenNone
really won't beNone
. It's little tricky if you don't want to modifycommit
: you'll need a second variable.If you are OK tweaking
commit
, you can stick with one variable, and simply callcast
when you return the value.