Profile picture

[Python] Pydantic으로 데이터 검증하기

JaehyoJJAng2024년 06월 04일

Pydantic

Pydantic은 파이썬의 Type Hint를 사용하여 데이터의 유효성을 검사하는 라이브러리이다.

파이썬에서 Type Hint는 변수 타입에 대한 힌트일 뿐, 다른 타입을 명시한다고 해서 문제가 발생하지는 않는다.

이러한 문제 때문에 pydantic은 파이썬에게 런타임에서 Type hint를 강제하여, 타입이 유효하지 않으면 error를 발생시킨다.


Pydantic은 기본적으로 유효성 검사를 위해 사용되지만, 엄밀하게 따지면 parsing 라이브러리이다.

출력 모델의 자료형이나 제약 조건을 보장하기 위한 수단으로 입력 데이터의 유효성을 검사한다.

결론적으로는 주로 사용되는 API의 I/O뿐만 아니라 다양한 함수를 안정적으로 사용하기 위한 도구로 볼 수 있겠다.


설치

pydantic은 파이썬 3.7 버전 이상이라면 문제없이 사용 가능하다.

pip install pydantic

예시

간단한 예제를 통해서 pydantic의 역할을 이해해보자.

from datetime import datetime
from pydantic import BaseModel

class Model(BaseModel):
    a: int
    b: float
    c: str
    d: bool

명시된 자료형에 맞는 데이터가 들어온다면, 코드는 정상 동작한다.

external_data :dict[str,int|float|str|bool] = {
    "a": 10,
    "b": 10.5,
    "c": "Hello",
    "d": False
}

result: dict[str,int|float|str|bool] = Model(**external_data).model_dump()
print(result)
# 출력
{'a': 10, 'b': 10.5, 'c': 'Hello', 'd': False}

근데 여기서 다른 자료형이 들어오면 에러를 발생시키 사용자가 확인하기 쉽도록 명시해준다.

external_data :dict[str,int|float|str|bool] = {
    "a": "Hello",
    "b": 10.5,
    "c": "Hello",
    "d": False
}

result: dict[str,int|float|str|bool] = Model(**external_data).model_dump()
print(result)
# 출력
Traceback (most recent call last):
  File "/home/a-user/github/ORM/00_pydantic연습/main.py", line 18, in <module>
    result = Model(**external_data).model_dump()
             ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/a-user/.pyenv/versions/orm/lib/python3.11/site-packages/pydantic/main.py", line 193, in __init__
    self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Model
a
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='Hello', input_type=str]
    For further information visit https://errors.pydantic.dev/2.8/v/int_parsing

만약 예외적으로 처리가 가능한 값이라면 자동으로 명시된 자료형에 맞춰주어 코드의 안정성을 높여준다.

external_data :dict[str,int|float|str|bool] = {
    "a": "10",
    "b": 10.5,
    "c": "Hello",
    "d": False
}

result: dict[str,int|float|str|bool] = Model(**external_data).model_dump()
print(result)
# 출력
{'a': 10, 'b': 10.5, 'c': 'Hello', 'd': False}

심화

Making Models

Models는 BaseModel로부터 상속받은 Class로, parsing과 validation을 통해 정의된 필드와 제약을 보장해준다.

앞선 예시를 통해서 유효성 검사를 통해 자료형을 확인해준다는 것은 확인해봤으니

추가적으로 또 어떤 것들이 가능한지 다른 예시를 들어서 이해해보자.

from pydantic import BaseModel

DATA_TYPE = dict[str, str|int|list[str]|None] 

class Person(BaseModel):
    first_name: str
    middle_name: str | None
    last_name: str
    age: int
    interest: list[str] | None = []

data :DATA_TYPE = {
    "first_name": "Jaehyo",
    "last_name": "Lee", 
    "age": 27
}

result = Person(**data).model_dump()
print(result)

여기서 Person이라는 Model Class에 정의된 필드들을 자세하게 살펴봐보자.

  • first_name
    • str 타입의 변수를 의미. 그리고 반드시 필요한 필드
  • middle_name
    • str 타입의 변수를 의미. 반드시 필요하지 않은 필드, 설정하지 않으면 None으로 지정됨
  • last_name
    • str 타입의 변수를 의미. 그리고 반드시 필요한 필드
  • age
    • int 타입의 변수를 의미. 그리고 반드시 필요한 필드
  • interest
    • str 타입의 list를 의미. 반드시 필요하지 않은 필드, 설정하지 않으면 []으로 지정됨

Custom validation

validator decorator를 사용하여 자체적인 규칙을 만들어 유효성 검사를 진행할 수도 있다.

from pydantic import BaseModel, field_validator, ValidationError

class Person(BaseModel):
    name: str
    age: int
    
    @field_validator('name')
    def name_must_english(cls, v: str) -> str:
        assert v.encode().isalpha(), ValueError("Must me english")
        return v 

    @field_validator('age')
    def adult_check(cls, v: int) -> int:
        if v <= 19:
            raise ValueError("not an adult")
        return v

data = {
    "name": "이재효",
    "age": 27
}

try:
    result = Person(**data).model_dump()
    print(result)
except ValidationError as e:
    print(e)
# 출력
1 validation error for Person
name
  Assertion failed, Must me english [type=assertion_error, input_value='이재효', input_type=str]
    For further information visit https://errors.pydantic.dev/2.8/v/assertion_error

Making JSON from Model

Model로부터 JSON을 생성하는 것도 가능하다.

from pydantic import BaseModel

class Person(BaseModel):
    first_name: str
    last_name: str
    age: int

data = {
    "first_name": "Jaehyo",
    "last_name": "Lee",
    "age": 27
}

result = Person(**data).schema_json(indent=2)
print(result)
# 출력
{
  "properties": {
    "first_name": {
      "title": "First Name",
      "type": "string"
    },
    "last_name": {
      "title": "Last Name",
      "type": "string"
    },
    "age": {
      "title": "Age",
      "type": "integer"
    }
  },
  "required": [
    "first_name",
    "last_name",
    "age"
  ],
  "title": "Person",
  "type": "object"
}

Loading script...