I have a code where a Main class receives instances of a Child class as attributes. Those attributes are typed as Optional, because they are undefined at application startup, in which case they get initialised.
Methods of the Main instance rely in many places on methods and attributes of those Child objects.
Here is a simplified example of my code, which is synchronous but in my actual app it's all async hencewhy the child does not get instantiated in __init__
, and I have a separate initialise
method:
import typing
class Child():
def print_foo(self) -> None:
print("foo")
class Main():
def __init__(self) -> None:
self.child: typing.Optional[Child] = None
def initialise(self) -> None:
if not self.child:
self.child = Child()
def foo(self) -> None:
self.child.print_foo()
main = Main()
main.initialise()
main.foo()
I am trying to enforce proper type checking in my application pipeline using mypy.
My problem is that since those child attributes are Optional and initially undefined, mypy complains that
Item "None" of "Optional[Child]" has no attribute "print_foo" [union-attr]
I could check in every method that the relevant attribute is defined, such as
def foo(self) -> None:
if self.child:
self.child.print_foo()
else:
raise Exception("child not defined")
but I do not want to do that because that would mean repeating this code for every method that is encountering that problem, and there are many in my application.
One way I tried to solve it is using a decorator:
import typing
def check_defined(func):
def wrapper(*args, **kwargs):
_self = args[0]
if not _self.child:
raise Exception("child not defined")
else:
return func(*args, **kwargs)
return wrapper
class Child():
def print_foo(self) -> None:
print("foo")
class Main():
def __init__(self) -> None:
self.child: typing.Optional[Child] = None
def initialise(self) -> None:
if not self.child:
self.child = Child()
@check_defined
def foo(self) -> None:
self.child.print_foo()
main = Main()
main.initialise()
main.foo()
However this does not change the outcome of the type checking.
Therefore my question is twofold:
Your Main
class seems to be trying to be two things:
foo
methodfoo
method.These should probably be two separate classes.
from dataclasses import dataclass
from typing import Optional
class Child:
def print_foo(self) -> None:
print("foo")
@dataclass
class Main:
child: Child
def foo(self) -> None:
self.child.print_foo()
@dataclass
class MainBuilder:
child: Optional[Child] = None
def set_child(self) -> None:
self.child = Child()
def build(self) -> Main:
if self.child is None:
raise ValueError("Child not yet available")
# Type narrowing: if we reach this line,
# we can assume self.child is not None,
# and thus assume its type is Child, not Optional[Child]
return Main(self.child)
Now you can start by instantiating a MainBuilder
object, with set_child
replacing the old initialise
method.
mb = MainBuilder()
mb.set_child()
You create a Main
object not by calling Main
directly, but by calling mb.build
, which will raise an exception if you don't yet have a child. Once you have a Main
object, then it is guaranteed to be ready to call foo
.
main = mb.build()
main.foo()
You can instantiate Main
directly, but now a Child
argument to Main.__init__
is defined and not optional.
main2 = Main(Child())