pydantic 살펴보기

seoyeon hwang
9 min readSep 18, 2022

최근 파이썬으로 개발할 때 파이썬 타입 어노테이션을 통해 더 견고한 프로그래밍을 하기 위해 노력하고 있다. 확실히 타입 에러로 인한 문제점을 초기에 발견할 수 있고, 코드를 짤 때 조금 더 논리적으로 사고하게 되어 좋다고 느꼈다.

함수 타입 어노테이션, typing 모듈, enum, 사용자 클래스 타입 어노테이션 등을 사용하고 있지만 조금 더 편하게 사용하고자 라이브러리를 찾게 되었다. 오늘은 그 중에서 pydantic을 살펴보려고 한다 :)

pydantic이란?

pydantic은 파이썬 타입 어노테이션을 사용해서 데이터 유효성 검사와 설정 관리를 하는 라이브러리다. 런타임때 타입 힌트를 강제하고, 데이터가 유효하지 않을때 유저 친화적인 에러를 제공한다. 즉, pydantic을 사용하여 데이터가 어때야 하는지 정의하고, 유효한지 확인 할 수 있다.

pydantic은 validation이 아닌 parsing 라이브러리이기 때문에 input data를 정의된 타입으로 변환하여 output model의 타입과 제약 조건을 보장한다.

from pydantic import BaseModel   class Model(BaseModel):     
a: int
b: float
c: str
print(Model(a=3.1415, b=' 2.72 ', c=123).dict())
#> {'a': 3, 'b': 2.72, 'c': '123'}

예를 들어, a의 값으로 float이 들어와도 int로 parsing을 해서 output model의 타입을 보장해준다. 단, parsing이 불가능한 데이터 타입이 들어오면 validation error가 발생한다.

다른 파이썬 라이브러리와 동일하게 pip install pydantic 명령어로 설치할 수 있다.

장점

  • IDE plugin으로 제공되고 파이썬 타입 어노테이션과 유사하기 때문에 쉽게 사용할 수 있다.
  • 비슷한 라이브러리 중에서 가장 빠르다.
  • recursive model, typing 의 타입, validator 를 통해 복잡한 데이터 스키마를 정의하고, validation과 parsing을 할 수 있다.

model

1) basic model

pydantic에서 주로 객체를 정의하는 방법은 BaseModel상속받아 model을 정의하는 것이다. untrusted 데이터가 model에게 전달되어도 pydantic의 parsing과 validation이 정의된 필드 타입과 맞도록 보장한다.

from pydantic import BaseModel# User라는 model 정의
class User(BaseModel):
id: int
name = 'Jane Doe'
# User의 인스턴스 생성
user = User(id='123')
user_x = User(id='123.45')
  • User의 name 은 기본 값으로부터 타입을 추론할 수 있기 때문에 required 필드는 아니다.
  • 인스턴스가 초기화될때 parsing과 validation을 수행하고, output model 인스턴스가 유효하지 않으면 ValidationError 을 반환한다.

2) recursive model

더 복잡하고 계층적인 데이터 구조를 나타내기 위해 모델 자체를 타입으로 정의할 수 있다.

from typing import List, Optional
from pydantic import BaseModel
class Foo(BaseModel):
count: int
size: Optional[float] = None
class Bar(BaseModel):
apple = 'x'
banana = 'y'
class Spam(BaseModel):
foo: Foo
bars: List[Bar]
m = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])
print(m.dict())
"""
{
'foo': {'count': 4, 'size': None},
'bars': [
{'apple': 'x1', 'banana': 'y'},
{'apple': 'x2', 'banana': 'y'},
],
}
"""

3) ORM model

ORM 객체와 맵핑할 수 있는 pydantic model도 생성할 수 있다. 이때, 주의할 점은 아래와 같다.

  • pydantic model의 Config 클래스에 orm_mode = True 로 설정
  • from_orm 를 통해 ORM model을 pydantic model의 인스턴스로 생성
from typing import List 
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel, constr
Base = declarative_base()# 1. ORM 모델 정의
class CompanyOrm(Base):
__tablename__ = 'companies'
id = Column(Integer, primary_key=True, nullable=False)
public_key = Column(String(20), index=True, nullable=False, unique=True)
name = Column(String(63), unique=True)
domains = Column(ARRAY(String(255)))
# 2. pydantic 모델 정의
class CompanyModel(BaseModel):
id: int
public_key: constr(max_length=20)
name: constr(max_length=63)
domains: List[constr(max_length=255)]

# Config의 orm_mode를 True로 설정
class Config:
orm_mode = True
co_orm = CompanyOrm(id=123, public_key='foobar', name='Testing', domains=['example.com','foobar.com'])# from_orm으로 인스턴스 생성
co_model = CompanyModel.from_orm(co_orm)

validator

validator 데코레이터를 사용해서 custom validation과 오브젝트 사이의 복잡한 관계를 수행할 수 있다.

  • validator는 클래스 메소드이기 때문에 첫번째 인자는 클래스가 오고, 두번째 인자는 유효성을 검사할 필드값이 온다.
  • validator는 parsing한 값 또는 ValueError/TypeError/ AssertionError 을 반환한다.
  • 유효성 검사는 정의된 필드 순서대로 수행되기 때문에 passwordname 에 접근할 수 있지만, namepassword 에 접근할 수 없다.
from pydantic import BaseModel, ValidationError, validatorclass UserModel(BaseModel):     
name: str
username: str
password: str
@validator('name')
def name_must_contain_space(cls, v):
if ' ' not in v:
raise ValueError('must contain a space')
return v.title()
try:
UserModel(
name='samuel',
username='scolvin',
password='zxcvbn'
)
except ValidationError as e:
print(e)
"""
1 validation errors for UserModel
name
must contain a space (type=value_error)
"""

exporting model

model.foobar 와 같이 이름으로 모델의 속성에 접근할 수도 있지만 json이나 dict로 변환하여 모델에 접근할 수 도 있다.

1) model.dict()

model.dict()메소드를 사용해서 model을 dict로 변환할 수 있다. sub-model도 재귀적으로 dict로 변환된다.

  • include , exclude 인자를 통해 dict로 변환시 포함 또는 포함하지 않을 필드를 지정할 수 있다.
from pydantic import BaseModelclass BarModel(BaseModel):
whatever: int
class FooBarModel(BaseModel):
banana: float
foo: str
bar: BarModel
m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})# returns a dictionary:
print(m.dict())
"""
{
'banana': 3.14,
'foo': 'hello',
'bar': {'whatever': 123},
}
"""
print(m.dict(include={'foo', 'bar'})) #> {'foo': 'hello', 'bar': {'whatever': 123}}

2) model.json()

model.json() 메소드를 사용해서 model을 json으로 변환할 수 있다. pydantic은 json.dumps()와 달리 datetime, date, UUID와 같은 다양한 타입을 JSON으로 변환할 수 있다.

from datetime import datetime
from pydantic import BaseModel
class BarModel(BaseModel):
whatever: int
class FooBarModel(BaseModel):
foo: datetime
bar: BarModel
m = FooBarModel(foo=datetime(2032, 6, 1, 12, 13, 14), bar={'whatever': 123})
print(m.json())
#> {"foo": "2032-06-01T12:13:14", "bar": {"whatever": 123}}

--

--