For classes in general, you can access the __annotations__:

>>> class Foo:
...    bar: int
...    baz: str
...
>>> Foo.__annotations__
{'bar': <class 'int'>, 'baz': <class 'str'>}

This returns a dict mapping attribute name to annotation.

However, dataclasses use dataclasses.Field objects to encapsulate a lot of this information. You can use dataclasses.fields on an instance or on the class:

>>> import dataclasses
>>> @dataclasses.dataclass
... class Foo:
...     bar: int
...     baz: str
...
>>> dataclasses.fields(Foo)
(Field(name='bar',type=<class 'int'>,default=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD), Field(name='baz',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD))

NOTE:

Starting in Python 3.7, the evaluation of annotations can be postponed:

>>> from __future__ import annotations
>>> class Foo:
...     bar: int
...     baz: str
...
>>> Foo.__annotations__
{'bar': 'int', 'baz': 'str'} 

note, the annotation is kept as a string, this also affects dataclasses as well:

>>> @dataclasses.dataclass
... class Foo:
...     bar: int
...     baz: str
...
>>> dataclasses.fields(Foo)
(Field(name='bar',type='int',default=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD), Field(name='baz',type='str',default=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD))

So, just be aware, since this will become the standard behavior, code you write should probably use the __future__ import and work under that assumption, because in Python 3.10, this will become the standard behavior.

The motivation behind this behavior is that the following currently raises an error:

>>> class Node:
...    def foo(self) -> Node:
...        return Node()
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in Node
NameError: name 'Node' is not defined

But with the new behavior:

>>> from __future__ import annotations
>>> class Node:
...     def foo(self) -> Node:
...         return Node()
...
>>>

One way to handle this is to use the typing.get_type_hints, which I believe just basically eval's the type hints:

>>> import typing
>>> typing.get_type_hints(Node.foo)
{'return': <class '__main__.Node'>}
>>> class Foo:
...    bar: int
...    baz: str
...
>>> Foo.__annotations__
{'bar': 'int', 'baz': 'str'}
>>> import typing
>>> typing.get_type_hints(Foo)
{'bar': <class 'int'>, 'baz': <class 'str'>}

Not sure how reliable this function is, but basically, it handles getting the appropriate globals and locals of where the class was defined. So, consider:

(py38) juanarrivillaga@Juan-Arrivillaga-MacBook-Pro ~ % cat test.py
from __future__ import annotations

import typing

class Node:
    next: Node

(py38) juanarrivillaga@Juan-Arrivillaga-MacBook-Pro ~ % python
Python 3.8.5 (default, Sep  4 2020, 02:22:02)
[Clang 10.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> test.Node
<class 'test.Node'>
>>> import typing
>>> typing.get_type_hints(test.Node)
{'next': <class 'test.Node'>}

Naively, you might try something like:

>>> test.Node.__annotations__
{'next': 'Node'}
>>> eval(test.Node.__annotations__['next'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'Node' is not defined

You could hack together something like:

>>> eval(test.Node.__annotations__['next'], vars(test))
<class 'test.Node'>

But it can get tricky

Answer from juanpa.arrivillaga on Stack Overflow
Top answer
1 of 2
21

For classes in general, you can access the __annotations__:

>>> class Foo:
...    bar: int
...    baz: str
...
>>> Foo.__annotations__
{'bar': <class 'int'>, 'baz': <class 'str'>}

This returns a dict mapping attribute name to annotation.

However, dataclasses use dataclasses.Field objects to encapsulate a lot of this information. You can use dataclasses.fields on an instance or on the class:

>>> import dataclasses
>>> @dataclasses.dataclass
... class Foo:
...     bar: int
...     baz: str
...
>>> dataclasses.fields(Foo)
(Field(name='bar',type=<class 'int'>,default=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD), Field(name='baz',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD))

NOTE:

Starting in Python 3.7, the evaluation of annotations can be postponed:

>>> from __future__ import annotations
>>> class Foo:
...     bar: int
...     baz: str
...
>>> Foo.__annotations__
{'bar': 'int', 'baz': 'str'} 

note, the annotation is kept as a string, this also affects dataclasses as well:

>>> @dataclasses.dataclass
... class Foo:
...     bar: int
...     baz: str
...
>>> dataclasses.fields(Foo)
(Field(name='bar',type='int',default=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD), Field(name='baz',type='str',default=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f806369bc10>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD))

So, just be aware, since this will become the standard behavior, code you write should probably use the __future__ import and work under that assumption, because in Python 3.10, this will become the standard behavior.

The motivation behind this behavior is that the following currently raises an error:

>>> class Node:
...    def foo(self) -> Node:
...        return Node()
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in Node
NameError: name 'Node' is not defined

But with the new behavior:

>>> from __future__ import annotations
>>> class Node:
...     def foo(self) -> Node:
...         return Node()
...
>>>

One way to handle this is to use the typing.get_type_hints, which I believe just basically eval's the type hints:

>>> import typing
>>> typing.get_type_hints(Node.foo)
{'return': <class '__main__.Node'>}
>>> class Foo:
...    bar: int
...    baz: str
...
>>> Foo.__annotations__
{'bar': 'int', 'baz': 'str'}
>>> import typing
>>> typing.get_type_hints(Foo)
{'bar': <class 'int'>, 'baz': <class 'str'>}

Not sure how reliable this function is, but basically, it handles getting the appropriate globals and locals of where the class was defined. So, consider:

(py38) juanarrivillaga@Juan-Arrivillaga-MacBook-Pro ~ % cat test.py
from __future__ import annotations

import typing

class Node:
    next: Node

(py38) juanarrivillaga@Juan-Arrivillaga-MacBook-Pro ~ % python
Python 3.8.5 (default, Sep  4 2020, 02:22:02)
[Clang 10.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> test.Node
<class 'test.Node'>
>>> import typing
>>> typing.get_type_hints(test.Node)
{'next': <class 'test.Node'>}

Naively, you might try something like:

>>> test.Node.__annotations__
{'next': 'Node'}
>>> eval(test.Node.__annotations__['next'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'Node' is not defined

You could hack together something like:

>>> eval(test.Node.__annotations__['next'], vars(test))
<class 'test.Node'>

But it can get tricky

2 of 2
4

Check this out:

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

Point.__annotations__ returns {'x': <class 'int'>, 'y': <class 'int'>}.

Top answer
1 of 3
7

If I've understood your question correctly, you can do something like this::

import json
import dataclasses

@dataclasses.dataclass
class mySubClass:
  sub_item1: str
  sub_item2: str

@dataclasses.dataclass
class myClass:
  item1: str
  item2: mySubClass

  # We need a __post_init__ method here because otherwise
  # item2 will contain a python dictionary, rather than
  # an instance of mySubClass.
  def __post_init__(self):
      self.item2 = mySubClass(**self.item2)


sampleData = '''
{
  "item1": "This is a test",
  "item2": {
    "sub_item1": "foo",
    "sub_item2": "bar"
  }
}
'''

myvar = myClass(**json.loads(sampleData))
myvar.item2.sub_item1 = 'modified'
print(json.dumps(dataclasses.asdict(myvar)))

Running this produces:

{"item1": "This is a test", "item2": {"sub_item1": "modified", "sub_item2": "bar"}}

As a side note, this all becomes easier if you use a more fully featured package like pydantic:

import json
from pydantic import BaseModel

class mySubClass(BaseModel):
  sub_item1: str
  sub_item2: str

class myClass(BaseModel):
  item1: str
  item2: mySubClass

sampleData = '''
{
  "item1": "This is a test",
  "item2": {
    "sub_item1": "foo",
    "sub_item2": "bar"
  }
}
'''

myvar = myClass(**json.loads(sampleData))
myvar.item2.sub_item1 = 'modified'
print(myvar.json())
2 of 3
3

Without using any libraries other than the builtins:

import dataclasses
import json


@dataclasses.dataclass
class mySubClass:
    sub_item1: str
    sub_item2: str


@dataclasses.dataclass
class myClass:
    item1: str
    item2: mySubClass

    @classmethod
    def from_json(cls, string: str):
        data: dict = json.loads(string)
        if isinstance(data['item2'], dict):
            data['item2'] = mySubClass(**data['item2'])
        return cls(**data)

    def json(self):
        return json.dumps(self, default=lambda o: o.__dict__)


sampleData = '''
{
  "item1": "This is a test",
  "item2": {
    "sub_item1": "foo",
    "sub_item2": "bar"
  }
}
'''

myvar = myClass.from_json(sampleData)
myvar.item2.sub_item1 = 'modified'
print(myvar.json())

Which becomes a bit easier, using a ser/de library like dataclass-wizard, or dataclasses-json:

import dataclasses

from dataclass_wizard import JSONWizard


@dataclasses.dataclass
class myClass(JSONWizard):
    item1: str
    item2: 'mySubClass'

    # optional
    @property
    def json(self, indent=None):
        return self.to_json(indent=indent)


@dataclasses.dataclass
class mySubClass:
    sub_item1: str
    sub_item2: str


sampleData = '''
{
  "item1": "This is a test",
  "item2": {
    "sub_item1": "foo",
    "sub_item2": "bar"
  }
}
'''

c = myClass.from_json(sampleData)
print(c.json)

Disclaimer: I am the creator and maintenor of this library.

🌐
PyPI
pypi.org › project › dataclasses-json
dataclasses-json
JavaScript is disabled in your browser. Please enable JavaScript to proceed · A required part of this site couldn’t load. This may be due to a browser extension, network issues, or browser settings. Please check your connection, disable any ad blockers, or try using a different browser
🌐
Tom's Blog
tomaugspurger.net › posts › serializing dataclasses
Serializing Dataclasses | Tom's Blog
August 31, 2024 - A plain “roundtrip” like json.loads only gets us part of the way there: >>> json.loads(serialized) {'version': '3', 'array_metadata': {'shape': [2, 2], 'timestamp': '2000-01-01T00:00:00'}, 'encoder': {'value': 1}, 'attributes': {'foo': 'bar'}, 'name': None} We have plain dictionaries instead of instances of our dataclasses and the timestamp is still a string. In short, we need to decode all the values we encoded earlier.
🌐
CodeRivers
coderivers.org › blog › python-get-all-keys-from-dataclass
Python: Getting All Keys from a Dataclass - CodeRivers
January 23, 2025 - This code creates a JSON - serializable dictionary by getting the keys of the Car dataclass and their corresponding values. ... If you need to access additional metadata about the fields (such as type hints), __dataclass_fields__ is the better choice as it provides more information. If you only need the attribute names and want a more general-purpose approach that also works with non-dataclass objects, vars() can be used.
🌐
Python.org
discuss.python.org › python help
Convert several layers deep nested JSON to instance of a class with nested class attributes - Python Help - Discussions on Python.org
March 5, 2024 - Hello, I’m trying to write tests using pytest library. I have a JSON file that I want to have as an input so I made a pytest.fixture to return the data converted to PlanRequest class instance, because that’s what I need to work with in my test function. @pytest.fixture def test_data(): file = os.path.join(os.path.dirname(__file__), 'test_data.json') with open(file) as json_file: json_data = json.load(json_file) job = PlanRequest(json_data) _preprocess_job(job) ...
Find elsewhere
🌐
Medium
medium.com › @emirhalici › unlocking-the-power-of-python-data-classes-w-json-serialization-3e5a24d98e84
Unlocking the Power of Python Data Classes w/ Json Serialization | by Emir Halici | Medium
February 18, 2024 - For this exact reason our models all have default value. Without either making them null by default or providing valid default values, json constructor would not work. import dataclasses from typing import Type, TypeVar import re T = TypeVar("T") # Needed for type inference def snake_to_camel(input: str) -> str: # Can swap out with more sophisticated implementation if needed camel_cased = "".join(x.capitalize() for x in input.lower().split("_")) if camel_cased: return camel_cased[0].lower() + camel_cased[1:] else: return camel_cased __camel_to_snake_pattern = re.compile(r"(?<!^)(?=[A-Z])") def camel_to_snake(input: str) -> str: return __camel_to_snake_pattern.sub("_", input).lower() @dataclasses.dataclass class PersonModel: age: int = 20 first_name: str = "Brad" last_name: str = "Pitt" def to_json(self, include_null=False) -> dict: """Converts this to json.
🌐
Towards Data Science
towardsdatascience.com › home › latest › working with python dataclasses and dataclass wizard
Working with Python Dataclasses and Dataclass Wizard | Towards Data Science
January 17, 2025 - To add extra flavor to our example, _attr9 in ClassD is a private attribute that we don’t want to dump into our dictionary representation, and attr2 in ClassA is an Enum object. from dataclasses import dataclass from typing import Dict, Optional, List, Union from dataclass_wizard import fromdict, json_field class AttrType(Enum): TYPE1 = "type1" TYPE2 = "type2" @dataclass(slots=True) class ClassA: attr1: Optional[int] = json_field("attr1", default=None) attr2: Optional[AttrType] = json_field("attr2", default=None) @dataclass(slots=True) class ClassB: attr3: Optional[int] = json_field("attr3",
🌐
Thenerdnook
thenerdnook.io › p › json-with-dataclasses
How to Handle JSON in Python the Clean Way with dataclasses-json
January 14, 2026 - It lets you take structured data like JSON from an API or a config file and turn it straight into proper Python objects without writing a bunch of extra glue code. One of the best parts about this library is how little effort it takes to use. You do not have to learn a whole new system or way of thinking. You write normal Python dataclasses, add a decorator, and suddenly you can turn JSON into objects and objects back into JSON with a single call.
🌐
Readthedocs
fancy-dataclass.readthedocs.io › en › latest › json
JSON conversion - Fancy Dataclass
This will suppress fields in an output dict or JSON whose values match the class's default value. While this is often helpful to keep the output smaller in size, it is sometimes better to be explicit. To override this behavior, you can set suppress_defaults=False. @dataclass class A(JSONDataclass): x: int = 5 @dataclass class B(JSONDataclass, suppress_defaults=False): x: int = 5 print(A().to_json_string()) {} print(B().to_json_string()) {"x": 5}
🌐
Stack Overflow
stackoverflow.com › questions › 74670567 › dataclass-json-get-class-name-inside-the-json-dict-object
python - dataclass_json : get class name inside the json/dict object - Stack Overflow
import json def to_my_custom_json(person: Person) -> str: return json.dumps({"Person": person.to_dict()}) ... @dataclass_json @dataclass class Person: name: str def to_my_custom_json(self) -> str: return json.dumps({"Person": self.to_dict()}) ...
Top answer
1 of 2
2

The dataclasses module doesn't provide built-in support for this use case, i.e. loading YAML data to a nested class model.

In such a scenario, I would turn to a ser/de library such as dataclass-wizard, which provides OOTB support for (de)serializing YAML data, via the PyYAML library.

Disclaimer: I am the creator and maintener of this library.

Step 1: Generate a Dataclass Model

Note: I will likely need to make this step easier for generating a dataclass model for YAML data. Perhaps worth creating an issue to look into as time allows. Ideally, usage is from the CLI, however since we have YAML data, it is tricky, because the utility tool expects JSON.

So easiest to do this in Python itself, for now:

from json import dumps

# pip install PyYAML dataclass-wizard
from yaml import safe_load
from dataclass_wizard.wizard_cli import PyCodeGenerator

yaml_string = """
account: 12345
clusters:
  - name: cluster_1
    endpoint: https://cluster_2
    certificate: abcdef
  - name: cluster_1
    endpoint: https://cluster_2
    certificate: abcdef
"""

py_code = PyCodeGenerator(experimental=True, file_contents=dumps(safe_load(yaml_string))).py_code
print(py_code)

Prints:

from __future__ import annotations

from dataclasses import dataclass

from dataclass_wizard import JSONWizard


@dataclass
class Data(JSONWizard):
    """
    Data dataclass

    """
    account: int
    clusters: list[Cluster]


@dataclass
class Cluster:
    """
    Cluster dataclass

    """
    name: str
    endpoint: str
    certificate: str

Step 2: Use Generated Dataclass Model, alongside YAMLWizard

Contents of my_file.yml:

account: 12345
clusters:
  - name: cluster_1
    endpoint: https://cluster_5
    certificate: abcdef
  - name: cluster_2
    endpoint: https://cluster_7
    certificate: xyz

Python code:

from __future__ import annotations

from dataclasses import dataclass
from pprint import pprint

from dataclass_wizard import YAMLWizard


@dataclass
class Data(YAMLWizard):
    account: int
    clusters: list[Cluster]


@dataclass
class Cluster:
    name: str
    endpoint: str
    certificate: str


data = Data.from_yaml_file('./my_file.yml')
pprint(data)
for c in data.clusters:
    print(c.endpoint)

Result:

Data(account=12345,
     clusters=[Cluster(name='cluster_1',
                       endpoint='https://cluster_5',
                       certificate='abcdef'),
               Cluster(name='cluster_2',
                       endpoint='https://cluster_7',
                       certificate='xyz')])
https://cluster_5
https://cluster_7
2 of 2
1

As Barmar points out in a comment, even though you have correctly typed the _clusters key in your AWSInfo dataclass...

@dataclass
class AWSInfo:
    _account: int
    _clusters: list[ClusterInfo]

...the dataclasses module isn't smart enough to automatically convert the members of the clusters list in in your input data into the appropriate data type. If you use a more comprehensive data model library like Pydantic, things will work like you expect:

import yaml
from pydantic import BaseModel

class ClusterInfo(BaseModel):
    name: str
    endpoint: str
    certificate: str

class AWSInfo(BaseModel):
    account: int
    clusters: list[ClusterInfo]


with open('clusters.yml', 'r') as fd:
    clusters = yaml.safe_load(fd)
a = AWSInfo(**clusters)

print(a.account) #prints 12345
print(a.clusters) #prints the dict of both clusters
print(a.clusters[0]) #prints the dict of the first cluster

#These prints fails with AttributeError: 'dict' object has no attribute '_endpoint'
print(a.clusters[0].endpoint)
for c in a.clusters:
    print(c.endpoint)

Running the above code (with your sample input) produces:

12345
[ClusterInfo(name='cluster_1', endpoint='https://cluster_2', certificate='abcdef'), ClusterInfo(name='cluster_1', endpoint='https://cluster_2', certificate='abcdef')]
name='cluster_1' endpoint='https://cluster_2' certificate='abcdef'
https://cluster_2
https://cluster_2
https://cluster_2
🌐
Ivergara
ivergara.github.io › deeper-dataclasses.html
Deeper into dataclasses - On data, programming, and technology
August 5, 2019 - We have to add/delete attributes/fields definitions and all references to them in the relevant methods. In this particular case, that would be six lines modified. The first part is the easy and the second one (very) annoying. We could do slightly better and only having to care for the first part if we want to address other dimensions. Let’s be slightly smarter and use more of the tools available in the dataclasses module.
🌐
Pydantic
docs.pydantic.dev › latest › concepts › dataclasses
Dataclasses - Pydantic Validation
Similarly to Pydantic models, arguments used to instantiate the dataclass are copied. To make use of the various methods to validate, dump and generate a JSON Schema, you can wrap the dataclass with a TypeAdapter and make use of its methods.
🌐
GeeksforGeeks
geeksforgeeks.org › python › convert-class-object-to-json-in-python
Convert class object to JSON in Python - GeeksforGeeks
July 23, 2025 - Explanation: json.dumps(p1.to_dict()) calls the to_dict() method of the Person class, which returns a dictionary representation of the object's attributes. This dictionary is then serialized into a JSON string using json.dumps(). Python’s dataclasses module provides a simple way to define classes with minimal boilerplate.
Top answer
1 of 4
31

This example shows only a name, type and value, however, __dataclass_fields__ is a dict of Field objects, each containing information such as name, type, default value, etc.

Using dataclasses.fields()

Using dataclasses.fields() you can access fields you defined in your dataclass.

fields = dataclasses.fields(dataclass_instance)

Using inspect.getmembers()

Using inspect.getmembers() you can access all fields in your dataclass.

members = inspect.getmembers(type(dataclass_instance))
fields = list(dict(members)['__dataclass_fields__'].values())

Complete code solution

import dataclasses
import inspect


@dataclasses.dataclass
class Test:
    a: str = "a value"
    b: str = "b value"


def print_data_class(dataclass_instance):

    # option 1: fields
    fields = dataclasses.fields(dataclass_instance)

    # option 2: inspect
    members = inspect.getmembers(type(dataclass_instance))
    fields = list(dict(members)['__dataclass_fields__'].values())

    for v in fields:
        print(f'{v.name}: ({v.type.__name__}) = {getattr(dataclass_instance, v.name)}')


print_data_class(Test())
# a: (str) = a value
# b: (str) = b value

print_data_class(Test(a="1", b="2"))
# a: (str) = 1
# b: (str) = 2
2 of 4
9

Also, you can use __annotations__, well, because data fields are always annotated. This is the essense of dataclasses usage.

It works with classes

    fields = list(Test.__annotations__)

and with instances

    fields = list(test.__annotations__)

There should be noted that it doesn't work with dataclass subclasses. Obviously. However, simplicity gives you fields names directly, without extra code for extraction from Field objects.

🌐
Python
docs.python.org › 3 › library › dataclasses.html
dataclasses — Data Classes
February 23, 2026 - To determine whether a field contains a default value, @dataclass will call the descriptor’s __get__() method using its class access form: descriptor.__get__(obj=None, type=cls). If the descriptor returns a value in this case, it will be used as the field’s default. On the other hand, if the descriptor raises AttributeError in this situation, no default value will be provided for the field.