Postcondition of a method with mypy

329 Views Asked by At

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.

2

There are 2 best solutions below

2
On

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)
0
On

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