Reputation: 107759
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.
Upvotes: 0
Views: 346
Reputation: 7549
Late answer, I know, but I just stumbled upon this question. How about this?
from typing import Optional
class IncompleteFoo(Exception):
pass
class Foo:
def __init__(self) -> None:
self.content = None #type: Optional[str]
def check_completeness(self) -> str:
content = self.content
if content is None:
raise IncompleteFoo
return content
def commit(self) -> str:
return self.check_completeness()
MyPy seems happy with this. The following also seems perfectly acceptable:
from typing import Optional
class IncompleteFoo(Exception):
pass
class Foo:
def __init__(self) -> None:
self.content = None #type: Optional[str]
def check_completeness(self) -> str:
content = self.content
if content is None:
raise IncompleteFoo
return content
def commit(self) -> str:
self.content = self.check_completeness()
# Some more arbitrary code could go here.
return self.content
Upvotes: 0
Reputation: 531205
You have to use typing.cast
to tell mypy
that yes, this value that might have been None
really won't be None
. It's little tricky if you don't want to modify commit
: you'll need a second variable.
from typing import Optional, cast
class IncompleteFoo(Exception):
pass
class Foo:
def __init__(self) -> None:
self._content: Optional[str] = None
def check_completeness(self) -> None:
if self._content is None:
raise IncompleteFoo
self.content: str = cast(str, self._content)
def commit(self) -> str:
self.check_completeness()
return self.content
If you are OK tweaking commit
, you can stick with one variable, and simply call cast
when you return the value.
from typing import Optional, cast
class IncompleteFoo(Exception):
pass
class Foo:
def __init__(self) -> None:
self.content: Optional[str] = None
def check_completeness(self) -> None:
if self.content is None:
raise IncompleteFoo
def commit(self) -> str:
self.check_completeness()
return cast(str, self.content)
Upvotes: 0