23

I want to write a dataclass definition in Python, but can't refer to that same class inside the declaration.

Mainly what I want to achieve is the typing of this nested structure, as illustrated below:

 @dataclass
 class Category:
     title: str
     children: [Category] # I can't refer to a "Category"
  
 tree = Category(title='title 1', children=[
     Category('title 11', children=[]),
     Category('title 12', children=[])
 ])
martineau
  • 119,623
  • 25
  • 170
  • 301
RomanGodMode
  • 325
  • 3
  • 11

1 Answers1

29

Option #1

You can wrap class name in a string in order to forward-declare the annotation:

from dataclasses import dataclass
from typing import List


@dataclass
class Category:
    title: str
    children: List['Category']

Option #2

You can include a __future__ import so that all annotations by default are forward-declared as below. In this case, you can also eliminate the typing import and use new-style annotations in Python 3.7 and above.

from __future__ import annotations

from dataclasses import dataclass


@dataclass
class Category:
    title: str
    children: list[Category]
rv.kvetch
  • 9,940
  • 3
  • 24
  • 53
  • 3
    A warning about option #2: The "new" style of type hinting from [PEP 563](https://www.python.org/dev/peps/pep-0563/) was deferred from becoming the default for Python 3.10 because an alternative proposal, [PEP 649](https://www.python.org/dev/peps/pep-0649/) pointed out some disadvantages it has, and offered a slightly different solution. PEP 649 seems likely to be adopted eventually, but it's unclear at this time what the transition plan will be. It should offer the same benefits for forward references like this one, but it might require a different `__future__` import to enable. – Blckknght Nov 01 '21 at 20:50
  • 1
    A very good point! I wasn't aware of PEP 649, but that's certainly a proposal that's worth tracking. I am in particular in favor of the latter approach, introduced by PEP 563, which IMO is a perfect use case for resolving the recursive annotation issue as mentioned. – rv.kvetch Nov 01 '21 at 21:18
  • downside of `annotations`: it makes otherwise legal code below fail with `NameError: name 'Sub' is not defined`: `from __future__ import annotations # works if this is commented import dataclasses from dataclasses import dataclass from dataclasses_serialization.json import JSONSerializer def test1(): # works if this is at module scope instead of inside a function @dataclass class Sub: x: int @dataclass class Foo: s: Sub=None a=Foo(s=Sub(x=1)) j=JSONSerializer.serialize(a) a2=JSONSerializer.deserialize(Foo, j) test1()` – timotheecour Aug 05 '22 at 05:12
  • @timotheecour totally understand, and just to shed some light on why I believe that could be, but within the function I believe the class's global scope are just the local variables available to the function, rather than at the module level. So I think a workaround for this could be to update the module globals with the func locals. A quick fix and something I use in test cases frequently is `globals().update(locals())`, which essentially just updates module-level globals with any classes defined at the function (or test case) level up until that point. – rv.kvetch Aug 24 '22 at 18:37