Python pydantic이란? Python에서 데이터 검증과 설정을 관리해보자(Feat. FastAPI)
포스팅 개요
본 포스팅은 Python FastAPI에 대해서 정리하는 FastAPI 시리즈 포스팅입니다.
FastAPI 포스팅은 아래와 같은 순서로 정리할 예정입니다.
- Python FastAPI 시작하기 - FastAPI란? 설치 방법과 기본 예제(FastAPI example) (https://lsjsj92.tistory.com/648)
- FastAPI post 간단 예제와 비동기(Asynchronous) async 함수에 대해서 (https://lsjsj92.tistory.com/649)
- Python pydantic이란? Python에서 데이터 검증과 설정을 관리해보자(Feat. FastAPI) ( 본 포스팅 )
- FastAPI router란? router 사용법과 예제(fastapi router example)
- Pytorch 딥러닝(deep learning) 모델과 FastAPI를 활용한 FastAPI 예제(example)
- Docker와 FastAPI를 활용해 pytorch 딥러닝 모델 배포하기(deploy pytorch model using docker, fastapi)
본 글은 그 중 세 번째 글인 Python pydantic에 대해서 알아보는 글입니다. FastAPI에서 자주 활용되는 방법이기에, FastAPI 시리즈 글 중 하나로 넣었습니다.
해당 포스팅을 작성하면서 제가 참고한 글과 데이터는 다음과 같습니다.
- https://fastapi.tiangolo.com/
- https://github.com/tiangolo/fastapi
- https://docs.pydantic.dev/
- https://github.com/pydantic/pydantic
본 글에서 작성하는 모든 코드는 아래 github repository에 올려두었습니다.
포스팅 본문
포스팅 개요에서도 언급하였듯이 본 포스팅은 python pydantic에 대해서 정리하는 글입니다. pydantic이란 무엇이고 코드로 pydantic example 코드를 살펴보면서 pydantic 정리하고자 합니다.
Python Pydantic이란?
먼저, Python pydantic이란 무엇일까요? Pydantic 공식 document에는 다음과 같이 설명되어 있습니다.
" Data validation and settings management using Python type annotations.
pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.
Define how data should be in pure, canonical Python; validate it with pydantic. "
즉, 파이썬의 type annotation을 활용해서 data validation(검증)과 setting 관리를 해주는 라이브러리입니다. 또한, Pydantic은 runtime에 type hint를 적용하고 데이터가 invalid할 때 친숙한 오류를 제공한다고 합니다.
무슨말인지 잘 이해가 되지 않을 수도 있습니다만, 어쨌든 데이터를 검증해주겠다는 의미겠죠? 바로 코드를 살펴보면서 example을 봐보시죠.
Python Pydantic 간단한 예제(example)
사실 Pydantaic은 지난 포스팅(https://lsjsj92.tistory.com/649)에서도 잠깐 언급하였었습니다. 지난 포스팅에서 FastAPI post를 활용할 때 이 Pydantic을 활용해서 input data의 검증을 진행했었습니다.
하지만, 이번 예제에서는 FastAPI를 제외하고 Pydantic만 활용해보겠습니다.
1. 기본 사용 방법 ( using BaseModel )
from typing import List, Optional, Union
from datetime import datetime
from pydantic import BaseModel
class Movie(BaseModel):
mid: int
genre: str
rate: Union[int, float] # rate는 int와 float 둘 다 가능
tag: Optional[str] = None # tag는 str이고 기본 값은 None
date: Optional[datetime] = None # date는 datetime을 가지며 기본 값은 None
some_variable_list: List[int] = [] # 임의의 변수. 리스트 값을 가지고 그 값들은 int여야 함. 기본 값은 []
tmp_data = {
'mid' : '1',
'genre' : 'action',
'rate' : 1.5,
'tag' : None,
'date' : '2023-01-03 19:12:11'
}
tmp_movie = Movie(**tmp_data)
print(tmp_movie)
먼저, 위와 같은 코드를 작성해보겠습니다. 위 코드는 Movie라는 class를 정의했고 해당 class는 pydantic의 BaseModel을 상속해서 활용합니다. BaseModel은 객체를 정의하는 방법을 제공해줍니다.
Movie class를 보면 다음과 같은 특징으로 정의되어 있습니다.
- mid는 int 타입으로 정의
- genre는 strig 타입으로 정의
- rate는 int와 float 둘 다 가능하도록 정의
- tag는 str인데 기본 값은 None
- date는 datetime 형식인데 기본값은 None
- some_variable_list는 int로 구성된 List인데 기본 값은 비어있는 list ( [] )
이렇게 정의된 Movie에 tmp_data로 정의된 데이터를 제공해보겠습니다.
그럼 아래와 같은 결과를 확인할 수 있습니다.
위 처럼 정상적으로 값이 출력되었다면 값이 이상이 없이 동작된 것입니다.
그럼 만약에 위에서 정의한 type 말고 다른 type으로 데이터를 제공하면 어떻게 될까요?
위 처럼 rate 값을 1.5aa으로 이상한 데이터를 제공해줍니다.
이렇게 데이터를 제공해주고 실행하면 어떻게 될까요?
에러 메시지는 보면 rate에서 에러가 나왔다고 메세지가 출력됩니다. value는 integer나 float이어야 한다고 말이죠.
Pydantic은 위와 같이 데이터의 type이 올바르게 들어올 수 있도록 검증해줍니다.
2. 데이터 범위 설정하기 ( Using Field )
위 코드는 간단한 Pydantic 예제였습니다. Pydantic에서는 데이터 type을 검증할 수 있을 뿐만 아니라 Field라는 것을 활용해 데이터의 범위, 길이 제한 등을 설정할 수 있습니다. 바로 코드로 보시죠!
from typing import List, Optional, Union
from datetime import datetime
from pydantic import BaseModel, Field
class Movie(BaseModel):
mid: int
genre: str
rate: Union[int, float] # rate는 int와 float 둘 다 가능
tag: Optional[str] = None # tag는 str이고 기본 값은 None
date: Optional[datetime] = None # date는 datetime을 가지며 기본 값은 None
some_variable_list: List[int] = [] # 임의의 변수. 리스트 값을 가지고 그 값들은 int여야 함. 기본 값은 []
class User(BaseModel):
'''
gt : 설정된 값보다 큰
ge : 설정된 값보다 크거나 같은
lt : 설정된 값보다 작은
le : 설정된 값보다 작거나 같은
'''
uid: int
name: str = Field(min_length=2, max_length=7)
age: int = Field(gt=1, le=130)
tmp_movie_data = {
'mid' : '1',
'genre' : 'action',
'rate' : '1.5',
'tag' : None,
'date' : '2023-01-03 19:12:11'
}
tmp_user_data = {
'uid' : '100',
'name' : 'soojin',
'age' : '12'
}
tmp_movie = Movie(**tmp_movie_data)
tmp_user_data = User(**tmp_user_data)
print(tmp_movie.json())
print(tmp_user_data.json())
위 코드에서는 User라는 새로운 Class를 두었습니다. 이 User에서는 Field라는 것을 새로 두어서 여러가지 옵션을 추가해두었습니다. 그 옵션들은 다음과 같습니다.
- min_length : 최소 길이
- max_length : 최대 길이
- gt : 설정된 값보다 큰 정수
- ge : 설정된 값보다 크거나 같은 정수
- lt : 설정된 값보다 작은 정수
- le : 설정된 값보다 작거나 같은 정수
그 외에도 다양한 옵션이 있으니, 이는 pydantic document에서 참고하시면 됩니다.
즉, 위 코드에서는 name이라는 변수의 길이를 2~7로 설정하였고 age를 1 < age <= 130 으로 설정하였습니다.
코드를 실행한 결과는 위 사진과 같습니다.
값이 정상적으로 출력된 것을 확인할 수 있죠!
만약, 설정된 값을 벗어난 값을 부여한다면 어떻게될까요?
위처럼 soojin_1234를 통해 길이가 넘어가는 값을 제공해줍시다. 그러면 당연히 에러가 나올겁니다.
에러 메세지는 ensuer this value has at most 7 characters 라고 나오네요! 제가 설정한 범위가 7이라서 이런 에러가 나오는 것을 확인할 수 있습니다.
마찬가지로 age 값을 값이 넘어가는 것을 제공해주면 범위에 해당하는 에러가 나오게 됩니다.
3. BaseSettings
Pydantic에는 BaseModel 외에도 BaseSetting 이라는 것도 존재합니다. 이는 Settting과 관련된 것들을 정의할 수 있고 setting에 사용되는 env 와 같은 것들이 default로 읽어져 옵니다. 아래 예시를 보시죠
pydantic_example.py
from typing import List, Optional, Union
from datetime import datetime
from pydantic import BaseSettings
from pydantic import Field
class DBConfig(BaseSettings):
host: str = Field(default='127.0.0.1', env='db_host')
port: int = Field(default=3306, env='db_port')
class Config:
env_file = '.env_ex'
print(DBConfig().dict())
그리고 위 코드가 있는 같은 경로에 .env_ex 라는 것을 만들어서 아래와 같이 작성해두겠습니다.
.env_ex
db_port=3306123
db_host=123.123.123.123
한 번 pydantic_example.py를 실행해볼까요?
설정한 값이 출력되는 것을 확인할 수 있습니다.
또한, Setting에서는 validator라는 annotation을 붙여서 값을 체크할 수 있습니다. 예를 들어서 위의 코드에서 port에 @validator를 붙여서 아래와 같이 검증을 해보겠습니다.
class DBConfig(BaseSettings):
host: str = Field(default='127.0.0.1', env='db_host')
port: int = Field(default=3306, env='db_port')
class Config:
env_file = '.env_ex'
@validator("port")
def check_port(cls, port_input):
if port_input not in [3306, 8080]:
raise ValueError("port error")
return port_input
print(DBConfig().dict())
위 코드에서는 @validator("port")라고 설정해서 port 변수에 대한 valid를 체크합니다. 만약, port가 3306이나 8080이 아니라면 port error라는 Value Error가 나오도록 하는 것이죠. 이 상태에서 같은 .env를 실행시키면 어떻게 될까요? 확인해보겠습니다.
적절한 값이 들어가지 않았으므로 위에서 설정한 Value error가 나오게 됩니다.
이처럼 Pydantic의 BaseSetting을 이용해서 setting 값 또한 관리할 수 있습니다.
4. Pydantic example 응용
이제 위에서 정리한 Pydantic 내용을 총 정리하는 느낌으로 응용 pydantic example을 보고 본 포스팅을 마무리하고자 합니다.
아래 Pydantic 코드를 보시죠
class DBConfig(BaseSettings):
host: str = Field(default='127.0.0.1', env='db_host')
port: int = Field(default=3306, env='db_port')
class Config:
env_file = '.env_ex'
# 먼저 실행
@validator("host", pre=True)
def check_host(cls, host_input):
if host_input == 'localhost':
return "127.0.0.1"
return host_input
# validator error를 설정할 수 있음
@validator("port")
def check_port(cls, port_input):
if port_input not in [3306, 8080]:
raise ValueError("port error")
return port_input
class ProjectConfig(BaseModel):
project_name: str = 'soojin'
db_info: DBConfig = DBConfig()
data = {
'project_name': '이수진의 프로젝트',
'db_info': {
'host': 'localhost',
'port' : 3306
}
}
my_pjt = ProjectConfig(**data)
print(my_pjt.dict())
print(my_pjt.db_info)
위 코드는 앞서 배운 Pydantic의 BaseModel과 BaseSetting까지 전부 활용하였습니다. 또한, port와 host에 대한 validator를 추가해서 적절한 값인지 확인하는 간단한 로직을 추가하였습니다.
또한, data = {} 형태로 되어 있는 것이 있는데 이렇게 값을 넘겨주면 .env 와 같은 곳에서 데이터를 보는 것이 아니라 바로 값을 확인할 수 있습니다. 위 코드를 실행하면
이렇게 결과가 나오는 것을 확인할 수 있을겁니다.
FastAPI와 Pydantic
FastAPI에서는 Pydantic을 활용해서 데이터의 검증을 진행합니다. FastAPI에서 사용하는 데이터의 검증은 input으로 들어오는 데이터의 형태와 output으로 나가는 데이터의 형태를 검증할 수 있습니다. 간단하게 아래 예제를 통해 Pydantic이 FastAPI에서 어떻게 활용되는지 살펴보겠습니다.
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic import Field
app = FastAPI()
class DataInput(BaseModel):
name: str
class PredictOutput(BaseModel):
prob:float
prediction:int
@app.post("/pydantic", response_model=PredictOutput)
def pydantic_post(data_request: DataInput):
return {"prob": 0.1, "prediction" : 0}
위 코드는 pydantic_post에 /pydantic 이라는 경로로 들어오는 것에 대한 요청을 처리합니다. 이때, 요청될 때 data_request는 DataInput class 형태로 받게 합니다. DataInput class는 name이라는 변수를 가지고 있고 type은 string 값입니다.
또한, @app.post()에 response_model 값이 있는데 해당 값은 return 되는 형태를 지정할 수 있습니다. 여기에 PredictOutput 클래스를 지정해줍니다. PredictOutput class는 prob과 prediction이 있는데 prob은 float, prediction은 int type을 가지고 있습니다. 위 코드를 fastapi server로 실행시킨 후 값을 넘겨주면 아래와 같은 결과를 확인할 수 있습니다.
즉, 정상적으로 값이 넘어오는 것을 확인해볼 수 있습니다.
그럼 만약 다른 이상한 값을 넣어주면 어떻게 될까요?
위와 같이 prediction에 ㅁㄴㅇㄹ라는 이상한 값을 넘겨줬습니다.
prediction에는 integer 값이 들어가야 하는데 string이 들어간 것이죠!
그러면 위와 같이 에러가 나면서 valid integer를 넣으라는 메세지가 나오게 됩니다.
마무리
본 포스팅은 Python의 Pydantic에 대해서 정리한 포스팅입니다. Pydantic이란 무엇이고 결국 Pydantic은 파이썬의 type annotation을 활용해서 data validation(검증)과 setting 관리를 해주는 라이브러리라는 것을 알 수 있었고 그것들에 대한 예제(example)도 확인해봤습니다.
긴 글 읽어주셔서 감사합니다.