0

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?

1
  • It might make more sense to let the parent create the child in __post_init__, rather than expecting an incompletely-initialized child be passed to Parent.__init__. Commented Dec 11, 2024 at 14:22

1 Answer 1

1

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
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you. Since I don't want to write code to satisfy my tooling I decided to just disable it for the line parent: Parent = field(default=None, init=False) # type: ignore and it works perfectly.
NoParentSet is just None in disguise. If you're going to allow a Child without a "real" parent, just say so and use Optional[Parent].
It is not a "None" in disguise - it is a typed None (akim to None[Parent]) - not in this example, but there could be object types for which a null-value for the class with special properties can be needed. Like that "0" number in the numbers set, for example.
Otherwise, it is really simper to just use None,that is quite explicit in the answer, and it is the path the O.P. picked.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.