How to initialize a Pydantic object from field values given by position instead of name?
Asked Answered
T

5

6

I am not able to find a simple way how to initialize a Pydantic object from field values given by position (for example in a list instead of a dictionary) so I have written class method positional_fields() to create the required dictionary from an iterable:

from typing import Optional, Iterable, Any, Dict
from pydantic import BaseModel


class StaticRoute(BaseModel):
    if_name: str
    dest_ip: str
    mask: str
    gateway_ip: str
    distance: Optional[int]
    
    @classmethod
    def positional_fields(cls, values: Iterable) -> Dict[str, Any]:
        return dict(zip(cls.__fields__, values))


input_lines = """
  route ab 10.0.0.0 255.0.0.0 10.220.196.23 1
  route gh 10.0.2.61 255.255.255.255 10.220.198.38 1
""".splitlines()

for line in input_lines:
    words = line.split()
    if words and words[0] == 'route':
        sroute = StaticRoute(**StaticRoute.positional_fields(words[1:]))
        print(sroute)
if_name='ab' dest_ip='10.0.0.0' mask='255.0.0.0' gateway_ip='10.220.196.23' distance=1
if_name='gh' dest_ip='10.0.2.61' mask='255.255.255.255' gateway_ip='10.220.198.38' distance=1

Is there a more straightforward way of achieving this?

My method expects the __fields__ dictionary to have keys in the order the fields were defined in the class. I am not sure if this is guaranteed (assuming Python 3.6+).

Teutonism answered 25/10, 2021 at 16:50 Comment(2)
It seems to be intentional that reading of positional fields is not supported: Add support for positional arguments #116Teutonism
Related question: Allow positional arguments for BaseModel pydanticTeutonism
C
3

How about using dataclasses instead? Something like:

from typing import Optional

from pydantic.dataclasses import dataclass


@dataclass
class StaticRoute:
    if_name: str
    dest_ip: str
    mask: str
    gateway_ip: str
    distance: Optional[int]


words = "route if_name dest_ip mask gateway_ip 10".split()
print(StaticRoute(*words[1:])

# StaticRoute(if_name='if_name', dest_ip='dest_ip', mask='mask', gateway_ip='gateway_ip', distance=10)
Cateyed answered 25/10, 2021 at 17:25 Comment(2)
Now I notidced that it is dataclass from Pydantic. I have to check what are all the differences from the BaseModel which is being preferred in the documentation.Teutonism
I would like to remove my old upvote but because of StackOverflow explicit limitations I am not able to do that :( Pydantic dataclass is simply not what you normally want when using Pydantic. It has limitations when compared to BaseModel.Teutonism
R
1

You can use a combination of a custom RootType and NamedTuple, like so:

from pydantic import BaseModel
from typing import NamedTuple, Optional


class StaticRouteTuple(NamedTuple):
    if_name: str
    dest_ip: str
    mask: str
    gateway_ip: str
    distance: Optional[int]


class StaticRoute(BaseModel):
    __root__: StaticRouteTuple

    @property
    def route(self) -> StaticRouteTuple:
        return self.__root__


input_lines = """
  route ab 10.0.0.0 255.0.0.0 10.220.196.23 1
  route gh 10.0.2.61 255.255.255.255 10.220.198.38 1
""".splitlines()

for line in input_lines:
    words = line.split()
    if words and words[0] == "route":
        sroute = StaticRoute.parse_obj(words[1:]).route
        print(sroute)

If you don't want to use a custom RootType you can use instead pyandic.parse_obj_as, example:

from pydantic import parse_obj_as
sroute = parse_obj_as(StaticRouteTuple, words[1:])
Rojas answered 1/5, 2022 at 15:16 Comment(0)
T
0

The class method BaseModel.parse_obj() returns an object instance initialized by a dictionary. We can create a similar class method parse_iterable() which accepts an iterable instead.

from typing import Optional, Iterable, Any, Dict
from pydantic import BaseModel


class BaseModelExt(BaseModel):
    @classmethod
    def parse_iterable(cls, values: Iterable):
        return cls.parse_obj(dict(zip(cls.__fields__, values)))

    
class StaticRoute(BaseModelExt):
    if_name: str
    dest_ip: str
    mask: str
    gateway_ip: str
    distance: Optional[int]


input_lines = """
  route ab 10.0.0.0 255.0.0.0 10.220.196.23 1
  route gh 10.0.2.61 255.255.255.255 10.220.198.38 1
""".splitlines()

for line in input_lines:
    words = line.split()
    if words and words[0] == 'route':
        sroute = StaticRoute.parse_iterable(words[1:])
        print(sroute)

Note: We are still missing a confirmation if the order of BaseModel.__fields__ is guaranteed.

Teutonism answered 26/10, 2021 at 7:38 Comment(2)
Note that this would not validate if the number of items in the iterable are more than the number of fields in the modelPolyanthus
@whatthe Unfortunately it will not fail. Zipping iterables of different lengths produces an iterator of the shortest length and ignores the excessive items. (list(zip((1,2),(5,6,7))) == [(1,5),(2,6)]) --- In Python 3.10+ we can add the strict=True argument to zip() to make it fail if the iterables are not of equal lengths but this would make the StaticRoute.distance attribute non-optional. We would need to implement our own variant of zip() with asymmetric strictness to allow optional attributes and fail for excessive items.Teutonism
S
0

Currently Pydantic Field support kw_only attribute that will allow you to create your model with positional fields:

from pydantic import Field
from pydantic.dataclasses import dataclass


@dataclass
class MyModel:
    a: str = Field(kw_only=False)
    b: str = Field(kw_only=False)


model_arg = MyModel("test", "model")
model_kw = MyModel("test", b="model")
model_kw2 = MyModel(a="test", b="model")

print(model_arg, model_kw, model_kw2)
Sos answered 8/8, 2023 at 12:33 Comment(1)
Thanks but this is a dataclass compatibility feature. pydantic.dataclasses have limited functionality compared to pydantic.BaseModel also kw_only is not enabled by default so using Field(kw_only=False) is redundant here. See: docs.pydantic.dev/latest/usage/dataclassesTeutonism
R
0

This solution is fully Pydantic.

The input is some string that is returned via a serial connection, which I parse into the model by order.

Although this works, I don't know if the risk is worth the savings of code. If a future developer messes the order of the fields in the Model, it would be hard to debug the issue.

from pydantic import BaseModel


class IrisResponse(BaseModel):
    code: str
    char_t: str
    value: str
    char_s: str
    num1: str
    num2: str
    num3: str
    num4: str

    def __init__(self, value: str):
        super().__init__(**dict(zip(IrisResponse.__fields__, value.split())))


out = "03: T        90000 S= 0001 0001 0000 1000 "
result = IrisResponse(out)
print(result.__repr__())

Output:

IrisResponse(code='03:', char_t='T', value='90000', char_s='S=', num1='0001', num2='0001', num3='0000', num4='1000')
Renee answered 5/12, 2023 at 13:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.