Search code examples
pythonpython-typingmypypython-dataclasses

Access container class from contained class with python dataclasses


I have a parent class, that contains a child class. Both are implemented with python dataclasses. The classes look like this:

from __future__ import annotations
from dataclasses import dataclass


@dataclass
class Parent:
    name: str
    child: Child


@dataclass
class Child:
    parent: Parent

The goal is, to access the child class from the parent class, but also the parent class from the child class. At the same time I don't want to have to annotate either of the references as Optional.

Since the child object only exist with a parent object, this would possible:

from __future__ import annotations
from dataclasses import dataclass


@dataclass
class Parent:
    name: str
    child: Child

    def __post_init__(self):
        self.child.parent = self


@dataclass
class Child:
    parent: Parent = None


Parent(name="foo", child=Child())

However, since I am using mypy, it complains that Child.parent should be annotated with Optional[Parent]. In practice this is only true until after the __post_init__ call. How could I get around this issue?


Solution

  • Python doesn't have the concept of an "unitialized variable" - it either exists, and is defined with some value, or not. If you want to get the nefits of dataclass for the .parent attribute, it has to exist, even for brief moments, with None - and therefore None must be set as an allowed value for static type analysis purposes.

    There is no "workaround" that - it is how it should be. You can write Parent | None instead of Optional[Parent] - for the tooling it is just the same, but semantically it could be better.

    Ok - maybe there is a workaround: you might have a special value "Parent" - a kind of "Parent singleton" meaning the parent had not yet been set, and use that as the default value. But chances are this is just overengineering to satisfy the tooling, not the problem you have at hand.

    @dataclass
    class Parent:
        name: str
        child: Child
    
        def __post_init__(self):
            self.child.parent = self
    
    
    # make this a valid 'Parent' but
    # overrides the fields that won't
    # make sense in a  "null parent"
    class _NoParentSet(Parent):
        name: str = ""
        child: None = None
        def __post_init__(self):
            pass
    
    # create a single instance of that class:
    NoParentSet = _NoParentSet()
    
    
    @dataclass
    class Child:
        parent: Parent = NoParentSet