Search code examples
pythonpython-3.xpython-attrs

How to nest a list of attrs classes into an attrs class


I have a list of dicts and I'd like to use python-attrs to convert them into classes.

Here's the sample data:

[[characters]]
first_name = 'Duffy'
last_name = 'Duck'

[[characters]]
first_name = 'Bugs'
last_name = 'Bunny'

[[characters]]
first_name = 'Sylvester'
last_name = 'Pussycat'

[[characters]]
first_name = 'Elmar'
last_name = 'Fudd'

[[characters]]
first_name = 'Tweety'
last_name = 'Bird'

[[characters]]
first_name = 'Sam'
last_name = 'Yosemite'

[[characters]]
first_name = 'Wile E.'
last_name = 'Coyote'

[[characters]]
first_name = 'Road'
last_name = 'Runner'

This will then turn into a dictionary after reading the content:

{'characters': [{'first_name': 'Duffy', 'last_name': 'Duck'},
  {'first_name': 'Bugs', 'last_name': 'Bunny'},
  {'first_name': 'Sylvester', 'last_name': 'Pussycat'},
  {'first_name': 'Elmar', 'last_name': 'Fudd'},
  {'first_name': 'Tweety', 'last_name': 'Bird'},
  {'first_name': 'Sam', 'last_name': 'Yosemite'},
  {'first_name': 'Wile E.', 'last_name': 'Coyote'},
  {'first_name': 'Road', 'last_name': 'Runner'}]}

My classes look like this:

@define(kw_only=True)
class Character:
    first_name: str
    last_name: str

@define
class LooneyToons:
    characters: List[Character] = field(factory=list, converter=Character)

But it does not work: TypeError: Character.__init__() takes 1 positional argument but 2 were given

Of course I could modify the class a bit and use this code (which works):

@define
class LooneyToons:
    characters: List[Character]

> LooneyToons([Character(**x) for x in d['characters']])
LooneyToons(characters=[Character(first_name='Duffy', last_name='Duck'), Character(first_name='Bugs', last_name='Bunny'), Character(first_name='Sylvester', last_name='Pussycat'), Character(first_name='Elmar', last_name='Fudd'), Character(first_name='Tweety', last_name='Bird'), Character(first_name='Sam', last_name='Yosemite'), Character(first_name='Wile E.', last_name='Coyote'), Character(first_name='Road', last_name='Runner')])

But it would be more elegant (from my point of view) to handle this within the class LooneyToons by just giving d['characters'] as argument to the class.

Any hints for me? I already checked out cattrs but I don't get the point on how it may be useful in my case.


Solution

  • I'm the author of cattrs (and a maintainer of attrs ;).

    The problem is your converter argument on LooneyToons.characters. Here's the solution without it:

    from typing import List
    
    from attrs import define, field
    
    from cattrs import structure
    
    d = {
        "characters": [
            {"first_name": "Duffy", "last_name": "Duck"},
            {"first_name": "Bugs", "last_name": "Bunny"},
            {"first_name": "Sylvester", "last_name": "Pussycat"},
            {"first_name": "Elmar", "last_name": "Fudd"},
            {"first_name": "Tweety", "last_name": "Bird"},
            {"first_name": "Sam", "last_name": "Yosemite"},
            {"first_name": "Wile E.", "last_name": "Coyote"},
            {"first_name": "Road", "last_name": "Runner"},
        ]
    }
    
    
    @define(kw_only=True)
    class Character:
        first_name: str
        last_name: str
    
    
    @define
    class LooneyToons:
        characters: List[Character] = field(factory=list)
    
    
    structure(d, LooneyToons)
    

    If you insist on using a converter, it'll have to be a little more sophisticated than just Character (see other answer). But I suggest just removing it.