I think in reality I'm going to use a different design, where _attrib
is set in the construct and can therefore not be None
, however I'm fascinated to see if there's a way to make MyPy happy with this approach. I have a situation where an attribute (in this instance _attrib
) is set after the construction of the Thing
and there are a number of methods which require it to be set. It seamed reasonable, therefore, to spin up a teeny decorator to validate if the _attrib
was set and to chuck an exception if it's not. The below code, however, causes MyPy to still complain - although the error is Item "None" of "Optional[Any]" has no attribute "upper"
, so I think the type of self
is getting completely lost. I'm also getting Incompatible return value type (got "Callable[[Any, VarArg(Any), KwArg(Any)], Any]", expected "F")
on return inner
.
class AttribIsNone(Exception):
...
F = TypeVar("F", bound=Callable[...,Any]
def guard(func: F) -> F:
def inner(self, *args, **kwargs):
if self._attrib is None:
raise AttribIsNoneException()
return func(self, *args, **kwargs)
return inner
class Thing:
_attrib: str | None
@guard
def guarded_function(self) -> str:
return self._attrib.upper()
Ideally I think I'd bind F
to something like Callable[[Thing,...], Any]
but that's not a valid thing to do right now.
Similarly I tried creating a TypeVar for the return value:
R = TypeVar("R")
F = TypeVar("F", bound=Callable[...,R]
def guard(func: F) -> F:
def inner(self, *args, **kwargs) -> R:
...
However this is not allowed either.
PEP612 offers a ParamSpec
but I can't seem to work out how to construct one, and I'm not even sure if I did that it would help much!
Yes, you can do what you're looking for in the static typing system (whether it's a good idea in practice is based on your actual use case). Here's an example to make it work.
First, Thing
needs to be a generic with respect to its _attrib
type:
_AttribT = TypeVar("_AttribT", str, None)
class Thing(Generic[_AttribT]):
_attrib: _AttribT
Then, disallow @guard
from accepting self: Thing[None]
:
from __future__ import annotations
from typing import *
class AttribIsNoneException(Exception):
...
S = TypeVar("S", bound="Thing[Any]")
P = ParamSpec("P")
R = TypeVar("R")
_AttribT = TypeVar("_AttribT", str, None)
def guard(
func: Callable[Concatenate[Thing[str], P], R]
) -> Callable[Concatenate[Thing[str], P], R]:
def inner(self: S, /, *args: P.args, **kwargs: P.kwargs) -> R:
if self._attrib is None:
raise AttribIsNoneException()
return func(self, *args, **kwargs)
return inner
Instance methods of Thing
will then be forced to be annotated with self: Thing[str]
if decorated with @guard
:
class Thing(Generic[_AttribT]):
_attrib: _AttribT
@guard # Static typing errors
def bad_guarded_function(self) -> str:
return self._attrib.upper() # Static typing errors
@guard
def guarded_function(self: Thing[str]) -> str:
return self._attrib.upper()
In practice, you will need to ensure that you're accessing guarded_function
through a Thing[str]
instance and not a Thing[None]
instance. I don't really know what your code looks like, but here's one possible implementation:
class Thing(Generic[_AttribT]):
_attrib: _AttribT
def __new__(cls, attrib: _AttribT) -> Thing[_AttribT]:
thing: Thing[_AttribT] = super().__new__(cls)
thing._attrib = attrib
return thing
@guard
def guarded_function(self: Thing[str]) -> str:
return self._attrib.upper()
>>> string_thing = Thing("asdfjkl")
>>> string_thing.guarded_function()
>>>
>>> none_thing = Thing(None)
>>> none_thing.guarded_function() # mypy: Invalid self argument "Thing[None]" to attribute function "guarded_function" with type "Callable[[Thing[str]], str]" [misc]