2

How to make it raise an exception on setting a misspelled fields in a @dataclass-decorated Python class?

I want a practical way to do this. Do I need to write my own decorator instead?

@dataclass
class C(object):
    x: int = 1

obj = C()
obj.y = 2  # should raise an exception
porton
  • 5,214
  • 11
  • 47
  • 95

3 Answers3

8

One straightforward way (which works with any class) is to define __slots__:

In [1]: from dataclasses import dataclass

In [2]: @dataclass
   ...: class Foo:
   ...:     __slots__ = 'bar','baz'
   ...:     bar: int
   ...:     baz: int
   ...:

In [3]: foo = Foo(42, 88)

In [4]: foo.biz = 10
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-d52b60444257> in <module>()
----> 1 foo.biz = 10

AttributeError: 'Foo' object has no attribute 'biz'

The purpose of slots is to serve as a small optimization. It allows the instances of the class to use a symbol table instead of a dict as the namespace of the class. It increases the speed of attribute access slightly, and can significantly improve the per-instance memory usage (because the instance doesn't carry around a dict underneath the hood), however, it disallows dynamic attribute setting.

This is actually my favorite feature of __slots__.

Note, you must take care when using inheritance with slots, at least, if you want subclasses to retain the slots behavior.

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
1

One way to do so is to mark the dataclass as frozen:

>>> from dataclasses import dataclass
>>> @dataclass(frozen=True)
... class C:
...     x: int = 1
...     
>>> c = C()
>>> c.y = 1
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'y'

But note that this makes all existing attributes, like x in this case, read-only as well.

Arne
  • 17,706
  • 5
  • 83
  • 99
0

You can provide your own __setattr__() method that only allows assignment to known fields.

Example:

@dataclass
class C:
    x: int = 1

    def __setattr__(self, k, v): 
        if k not in self.__annotations__: 
            raise AttributeError(f'{self.__class__.__name__} dataclass has no field {k}')
        super().__setattr__(k, v)

In then fails/works like this:

[ins] In [3]: obj = C() 
         ...: obj.y = 2                                                                                                                                                                       
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-7a568eb098b1> in <module>
      1 obj = C()
----> 2 obj.y = 2

<ipython-input-2-d30972f86fbb> in __setattr__(self, k, v)
      5     def __setattr__(self, k, v):
      6         if k not in self.__annotations__:
----> 7             raise AttributeError(f'{self.__class__.__name__} dataclass has no field {k}')
      8         super().__setattr__(k, v)
      9 

AttributeError: C dataclass has no field y

[ins] In [4]: obj.x = 23                                                                                                                                                                      

[ins] In [5]: obj.x                                                                                                                                                                           
Out[5]: 23
maxschlepzig
  • 35,645
  • 14
  • 145
  • 182