Reputation: 203
I'm experiencing some frustration with mypy strict optional checking making code less legible.
The situation is that I'm instantiating a class representing a sequence of zero or more objects which has some Optional members (specifically, they are None if the instance represents an empty sequence). These optional variables are either all None or all not None, and when I use one of them I would like to be able to call a clearly-named function which checks their None-ness rather than having to explicitly check them in the function which uses them, as the latter option is making the code less clear.
I have produced a minimal example which reproduces the issue.
from typing import Optional
class ItemSequence:
def __init__(self, sequence_length: Optional[int]) -> None:
self.sequence_length = sequence_length
def is_empty(self) -> bool:
return self.sequence_length is None
def add_to_length_with_direct_check(self, x: int) -> Optional[int]:
if self.sequence_length is None:
return None
return self.sequence_length + x
def add_to_length_with_indirect_check(self, x: int) -> Optional[int]:
if self.is_empty():
return None
return self.sequence_length + x
When run through mypy, it returns the following:
mypy_test.py:19: error: Unsupported operand types for + ("None" and "int")
mypy_test.py:19: note: Left operand is of type "Optional[int]"
...with line 19 being the addition operation in add_to_length_with_indirect_check()
. Clearly mypy is not carrying the type-narrowing of sequence_length
out of is_empty()
. Is there a way to make it do so, or am I going to need to explicitly type-narrow each Optional member variable before I use it?
I suppose I could make ItemSequence
abstract and subclass it into something like ItemSequenceEmpty
and ItemSequenceNonEmpty
, which would allow the use of a TypeGuard, but that seems like a lot of overhead for what's actually being achieved. The other option that has been discussed is making empty sequences invalid and variables which hold sequences Optional, which would present its own complications elsewhere in the codebase.
Upvotes: 1
Views: 449
Reputation: 7943
Well, formally you can (gist mine). I'd not advice such solution, but...
from typing import Optional, TypeGuard, Protocol
class _IntLength(Protocol):
"""Something with numeric (not None) length"""
sequence_length: int
class ItemSequence:
def __init__(self, sequence_length: Optional[int]) -> None:
self.sequence_length = sequence_length
def add_to_length_with_direct_check(self, x: int) -> Optional[int]:
if self.sequence_length is None:
return None
return self.sequence_length + x
def add_to_length_with_indirect_check(self, x: int) -> Optional[int]:
if not is_not_empty(self):
return None
return self.sequence_length + x
def is_not_empty(obj: ItemSequence) -> TypeGuard[_IntLength]:
"""Check whether ``obj`` does not have ``None`` length."""
return obj.sequence_length is not None
It is not a hack, but intended usage of TypeGuard
. However, it is hard to understand and maintain, so it is useful only if you use this check multiple times and are 100% sure that this approach reads better. You can make is_not_empty
a class method too, but than it will be
class ItemSequence:
...
def is_not_empty(self, obj: 'ItemSequence') -> TypeGuard[_IntLength]:
...
and is used like if self.is_not_empty(self)
, which is nonsense IMO. TypeGuard
on self is not supported by design.
Upvotes: 1