4

Is there a way to set a pydantic model from a list? I tried this and it didn't work for me. If it's not possible with pydantic, what is the best way to do this if I still need type validation and conversion, constraints, etc.? Order is important here.

from pydantic import BaseModel
from datetime import date


class User(BaseModel):
    id: int
    name = 'John Doe'
    sex: str
    money: float = None
    dt: date


data = [1, 'Tike Myson', 'male', None, '2022-01-20']
user = User(*data)

>>> TypeError: __init__() takes exactly 1 positional argument (6 given)
Superbman
  • 787
  • 1
  • 8
  • 24

2 Answers2

3

I partially answered it here: Initialize FastAPI BaseModel using non-keywords arguments (a.k.a *args) but I'll give here more dynamic options.

Option 1: use the order of the attributes

Your case has the problem that Pydantic does not maintain the order of all fields (depends at least on whether you set the type). If you specify the type of name then this works:

from pydantic import BaseModel
from datetime import date


class User(BaseModel):
    id: int
    name: str = 'John Doe'
    sex: str
    money: float = None
    dt: date

    def __init__(self, *args):
        
        # Get a "list" of field names (or key view)
        field_names = self.__fields__.keys()
        
        # Combine the field names and args to a dict
        # using the positions.
        kwargs = dict(zip(field_names, args))
        
        super().__init__(**kwargs)
        

data = [1, 'Tike Myson', 'male', None, '2022-01-20']
user = User(*data)

Option 2: set the order as class variable

This has the downside of not being as dynamic but does not have the problem of the order being undesirable.

from pydantic import BaseModel
from datetime import date


class User(BaseModel):
    id: int
    name = 'John Doe'
    sex: str
    money: float = None
    dt: date
        
    field_names: ClassVar = ('id', 'name', 'sex', 'money', 'dt')

    def __init__(self, *args):
                
        # Combine the field names and args to a dict
        # using the positions.
        kwargs = dict(zip(self.field_names, args))
        
        super().__init__(**kwargs)
        

data = [1, 'Tike Myson', 'male', None, '2022-01-20']
user = User(*data)

Option 3: just hard-code them in the __init__

This is similar to option 2 but a bit simple (and less reusable).

from pydantic import BaseModel
from datetime import date
from typing import ClassVar


class User(BaseModel):
    id: int
    name = 'John Doe'
    sex: str
    money: float = None
    dt: date

    def __init__(self, id, name, sex, money, dt):
        super().__init__(id=id, name=name, sex=sex, money=money, dt=dt)
        

data = [1, 'Tike Myson', 'male', None, '2022-01-20']
user = User(*data)

Option 4: Without overriding

One more solution but this does not require overriding the __init__. However, this requires more code when creating an instance:

from pydantic import BaseModel
from datetime import date


class User(BaseModel):
    id: int
    name = 'John Doe'
    sex: str
    money: float = None
    dt: date


data = [1, 'Tike Myson', 'male', None, '2022-01-20']

# Combine the field names and data
field_names = ['id', 'name', 'sex', 'money', 'dt']
kwargs = dict(zip(field_names, data))

# Create an instance of User from a dict
user = User(**kwargs)

Note for options 1, 2 & 3

If you have multiple places where you need this, create a base class that has the __init__ overridden and subclass that.

miksus
  • 2,426
  • 1
  • 18
  • 34
0

To get the User object, you can do as below

user = User(
    id = 1,
    name = 'Tike Myson',
    sex = 'male',
    money = None,
    dt = '2022-01-20',
)

But you necessarily want to set it from a list, then you use something similar to the below example,

data = [1, 'Tike Myson', 'male', None, '2022-01-20']
zipped_data = dict(zip(User.__fields__.keys(), data))
user = User.parse_obj(zipped_data)

if this is also not sufficient, you could probably write a generic helper function to do the above work.