8

I'm new to Pydantic and trying to understand how/if I can create a new class instance. I've read through the Pydantic documentation and can't find an example doing anything similar.

My python code (prior to Pydantic) looks like:

class Person:
    def __init__(self, id):
        db_result = get_db_content(id)    #lookup id in a database and return a result
        self.name = db_result['name']      
        self.birth_year = db_result['birth_year']

p1 = Person(1234)
print(p1.name)

What would the corresponding code in Pydantic look like if I want to create a Person instance based on an id? Also, is it possible with Pydantic to have multiple constructors for the same
class. For example:

p1 = Person(1234)
p2 = Person("Jane Doe")
user2558918
  • 103
  • 1
  • 2
  • 4

5 Answers5

12

I'm not sure if this is the most "pydantic" way to do things, but one approach to solving this problem is to use classmethods.

class Person(BaseModel):
    id: int
    name: str
    birth_year: int

    @classmethod
    def from_id(cls, id: int) -> "Person":
        db_result = get_db_content(id)    #lookup id in a database and return a result
        return cls(**db_result)

p1 = Person.from_id(1234)
print(p1.name)
Mark Rogers
  • 96,497
  • 18
  • 85
  • 138
James P
  • 1,118
  • 1
  • 11
  • 13
6

You could use a standard __init__ for this:

from typing import Optional

from pydantic import BaseModel

def get_db_content(id):
    return {
        'name': f'hello {id}',
        'birth_year': 2010,
    }

class Person(BaseModel):
    id: str
    name: Optional[str]
    birth_year: Optional[int]
    def __init__(self, *a, **kw):
        super().__init__(*a, **kw)
        db_result = get_db_content(self.id)
        self.name = db_result['name']      
        self.birth_year = db_result['birth_year']

person = Person(id='15')
print(person.dict())
# {'id': '15', 'name': 'hello 15', 'birth_year': 2010}

Although, personally I wouldn't do that. Pydantic classes are meant to be used as parsers/validators, not as fully functional object entities. Calling DB methods from a class like this directly couples your class to the db code and makes testing more difficult.

I would do this instead:

from pydantic import BaseModel

def get_db_content(id):
    return {
        'name': f'hello {id}',
        'birth_year': 2010,
    }

class Person(BaseModel):
    id: str
    name: str
    birth_year: int

person = Person(id='15', **get_db_content('15'))
print(person.dict())
# {'id': '15', 'name': 'hello 15', 'birth_year': 2010}

NB: the code examples should be self-contained and work as-is.

edit: using standard dunder constructor instead of @root_validator

ierdna
  • 5,753
  • 7
  • 50
  • 84
  • The signature `def __init__(self, *a, **kw)` is horrible from a type annotation perspective and defeats pydantic's `init_typed = True` feature. – bluenote10 Aug 09 '22 at 14:22
  • @bluenote10 agreed. i didn't annotate it properly (let's say for brevity :) ), but that's another reason for not putting db calls inside `__init__` - so you don't have to worry about the inheritance chain – ierdna May 11 '23 at 14:41
2

This isn't really possible at the moment. You'll have to create the person with something like Person(get_db_content()).

In future you'll be able to have computed fields and context that would allow this.

SColvin
  • 11,584
  • 6
  • 57
  • 71
  • 1
    Hi. Is there any chance that it got better now? I couldn't find anything about computed fields and context in the docs. – Raz Jun 30 '21 at 12:38
  • 2
    Not yet, I think there might be an open pull request, but I don't have time to review it right now. – SColvin Jul 02 '21 at 10:08
2

One pattern you might consider is using a 'collection' class for your pydantic model.

Your model can (should) just be a data store / validator as folks have mentioned. And your collection class can act as your interface to the DB for fetching records, groups of records, performing finds, etc.. and returning your model objects, or lists of model objects.

Example pydantic class:

class Person(BaseModel):
    id: str
    name: Optional[str]
    birth_year: Optional[int]

Collection Class:

class PersonCollection:
    def __init__(self, db: pymongo.database.Database):
        self.collection = db.person_collection

    def find_people(self, criteria: dict) -> List[Person]:
        person_list: List[Person] = list()
        for person_data in self.collection.find(criteria):
            person_list.append(Person(**person_data))
        return person_list
    
     def save_person(self, person: Person):
        key = {"id": person.id}
        self.collection.update_one(
            filter=key,
            update={"$set": person.dict()},
            upsert=True
         )
1

Here is a slightly more robust version of ierdna's first answer:

from pydantic import BaseModel

def get_db_content(id):
    return {
        'name': f'ID: {id}',
        'birth_year': 2022
    }

class Person(BaseModel):
    name: str
    birth_year: int

    def __init__(self, id=None, **kwargs):
        if id is not None:
            # Look up the id from the database
            db_result = get_db_content(id)    
            kwargs.update(db_result)

        super().__init__(**kwargs)

p1 = Person(1234)
# Output: Person(name='ID 1234', birth_year=2022)

p2 = Person(name="Jack", birth_year=2020)
# Output: Person(name='Jack', birth_year=2020)

This has the benefit that if id is specified, constructor fetches from the database and if not, then the item is formed directly from given keyword arguments. This also supports setting the model as immutable as we don't set the attributes ourselves.

miksus
  • 2,426
  • 1
  • 18
  • 34