23

Suppose that class A has a member whose type is class B, and class B has a member whose type is class A.

In Scala or Kotlin, you can define the classes in any order without worries in this case because the firstly-defined class can use the secondly-defined class as usual, even in case/data classes.

However in Python, the following code

class A:
    b = B()

class B:
    a = A()     

throws a compile error because class B is not defined when class A is being defined.

You can work around this simple case, like in this answer

class A:
    pass

class B:
    a = A()

A.b = B()

However, this way does not work for data classes in Python because assigning members after the definition of data classes will not update the auto-generated methods of the data classes, which makes the usage of "data class" useless.

@dataclass
class A:
    b: B  # or `b: Optional[B]`

@dataclass
class B:
    a: A  # or `a: Optional[A]`

How can I avoid this problem?

Naetmul
  • 14,544
  • 8
  • 57
  • 81
  • I'm thinking of just closing this as a duplicate of [Type hints: solve circular dependency](//stackoverflow.com/q/33837918), because this is just another type hint circular dependency problem. – Martijn Pieters Oct 09 '18 at 20:54

3 Answers3

21

There are several ways to solve circular dependencies like this, see Type hints: solve circular dependency

You can always apply the decorator manually (and update the annotations), like @Nearoo's answer shows.

However, it might be easier to "forward declare" the class:

class A:
    pass

@dataclass
class B:
    a: A

@dataclass
class A:
    b: B

Or simply use a forward reference:

@dataclass
class B:
    a: 'A'

@dataclass
class A:
    b: B

The cleanest is to import Python 4.0's behavior (if you can):

from __future__ import annotations

@dataclass
class B:
    a: A

@dataclass
class A:
    b: B
asherbret
  • 5,439
  • 4
  • 38
  • 58
Acorn
  • 24,970
  • 5
  • 40
  • 69
  • The `__future__` import is nice, but doesn't seem to work if you have to do something like `a: A = field=(default_factory=A)`. Is there something additional that can be done in this case or is forward declaring the only option? – mboratko Oct 12 '18 at 15:22
  • 1
    Actually, looks like forward declaring doesn't work in this case either... – mboratko Oct 12 '18 at 16:25
4

You can achieve your goal by applying the dataclass decorator only after we injected the field b into A. For that, we simply have to add the type annotation into A's __annotations__-field

The following code solves your problem:

class A:
    b: None     # Note: __annotations__ only exists if >=1 annotation exists

@dataclass
class B:
    a: A

A.__annotations__.update(b=B) # Note: not the same as A.b: B
A = dataclass(A) # apply decorator

Concerning the safety and validity of this method, PEP 524 states that

..at the module or class level, if the item being annotated is a simple name, then it and the annotation will be stored in the __annotations__ attribute of that module or class. [This attribute] is writable, so this is permitted:

__annotations__['s'] = str

So adding a type annotation later on by editing __annotations__ is identical to defining it at the class definition.

Nearoo
  • 4,454
  • 3
  • 28
  • 39
0

It's even more verbose, but how about using a lambda for the field factory:

from __future__ import annotations

from dataclasses import dataclass, field


@dataclass
class A:
    b: B = field(default_factory=lambda: B())


@dataclass
class B:
    a: A = field(default_factory=lambda: A())


A()  # results in infinite recursion (as intended?)
Hau
  • 443
  • 6
  • 13