Skip to main content

Pydantic - 데이터 검증

학습 목표

  • BaseModel로 데이터 모델을 정의하고 자동 검증을 활용할 수 있다
  • Field로 필드 제약 조건을 설정할 수 있다
  • 커스텀 validator로 복잡한 검증 로직을 구현할 수 있다
  • BaseSettings로 환경 변수 기반 설정을 관리할 수 있다

왜 중요한가

Pydantic은 Python 타입 힌트를 기반으로 데이터 검증(Validation)과 직렬화를 수행하는 라이브러리입니다. FastAPI의 핵심 의존성이며, ML 파이프라인의 설정 관리, API 입출력 검증, 데이터 전처리 규칙 정의에 널리 사용됩니다.
# Pydantic 설치
pip install pydantic pydantic-settings

BaseModel 기본

1

모델 정의와 자동 검증

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int
    email: str
    is_active: bool = True  # 기본값

# 올바른 데이터
user = User(name="김철수", age=25, email="kim@example.com")
print(user.name)       # "김철수"
print(user.is_active)  # True (기본값 적용)

# 자동 타입 변환
user2 = User(name="이영희", age="30", email="lee@test.com")
print(type(user2.age))  # <class 'int'> (문자열 "30" → 정수 30)

# 잘못된 데이터 → ValidationError
try:
    bad_user = User(name="박민수", age="서른", email="park@test.com")
except Exception as e:
    print(f"검증 실패: {e}")
2

직렬화와 역직렬화

# 딕셔너리 변환
user_dict = user.model_dump()
print(user_dict)
# {'name': '김철수', 'age': 25, 'email': 'kim@example.com', 'is_active': True}

# JSON 변환
user_json = user.model_dump_json(indent=2)
print(user_json)

# 딕셔너리에서 생성
data = {"name": "홍길동", "age": 28, "email": "hong@test.com"}
user3 = User.model_validate(data)

# JSON에서 생성
json_str = '{"name": "홍길동", "age": 28, "email": "hong@test.com"}'
user4 = User.model_validate_json(json_str)

# 특정 필드만 포함/제외
partial = user.model_dump(include={"name", "email"})
excluded = user.model_dump(exclude={"is_active"})
3

중첩 모델

from pydantic import BaseModel

class Address(BaseModel):
    city: str
    zipcode: str

class Company(BaseModel):
    name: str
    address: Address

class Employee(BaseModel):
    name: str
    company: Company

# 중첩 딕셔너리에서 자동 생성
emp = Employee(
    name="김개발",
    company={
        "name": "배움AI",
        "address": {"city": "서울", "zipcode": "06000"},
    },
)
print(emp.company.address.city)  # "서울"

Field로 제약 조건 설정

from pydantic import BaseModel, Field

class TrainingConfig(BaseModel):
    """학습 설정 모델"""

    model_name: str = Field(
        ...,  # 필수 필드 (기본값 없음)
        description="모델 이름",
        examples=["bert-base", "gpt2"],
    )
    learning_rate: float = Field(
        default=0.001,
        gt=0,        # greater than 0 (0 초과)
        le=1.0,      # less than or equal 1.0 (1.0 이하)
        description="학습률",
    )
    batch_size: int = Field(
        default=32,
        ge=1,        # greater than or equal 1 (1 이상)
        le=512,      # 512 이하
    )
    epochs: int = Field(default=10, ge=1, le=1000)
    dropout: float = Field(default=0.1, ge=0.0, lt=1.0)
    tags: list[str] = Field(default_factory=list, max_length=10)

# 검증 성공
config = TrainingConfig(model_name="bert-base", learning_rate=0.0001)
print(config)

# 검증 실패
try:
    bad_config = TrainingConfig(
        model_name="bert",
        learning_rate=-0.01,  # 0 초과 조건 위반
    )
except Exception as e:
    print(f"검증 실패: {e}")
제약 조건설명적용 타입
gt초과 (greater than)숫자
ge이상 (greater than or equal)숫자
lt미만 (less than)숫자
le이하 (less than or equal)숫자
min_length최소 길이문자열, 리스트
max_length최대 길이문자열, 리스트
pattern정규식 패턴문자열

커스텀 Validator

from pydantic import BaseModel, Field, field_validator, model_validator

class ExperimentConfig(BaseModel):
    name: str
    learning_rate: float = Field(gt=0)
    batch_size: int = Field(ge=1)
    val_ratio: float = Field(default=0.2, ge=0, le=1)
    train_ratio: float = Field(default=0.8, ge=0, le=1)

    # 필드 단위 검증
    @field_validator("name")
    @classmethod
    def name_must_not_be_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("실험 이름은 비어있을 수 없습니다")
        return v.strip()

    @field_validator("batch_size")
    @classmethod
    def batch_size_must_be_power_of_two(cls, v: int) -> int:
        if v & (v - 1) != 0:
            raise ValueError(f"배치 크기는 2의 거듭제곱이어야 합니다: {v}")
        return v

    # 모델 전체 검증 (여러 필드 간 관계)
    @model_validator(mode="after")
    def check_ratios(self) -> "ExperimentConfig":
        total = self.val_ratio + self.train_ratio
        if abs(total - 1.0) > 0.001:
            raise ValueError(
                f"train_ratio + val_ratio = {total:.3f}, 1.0이어야 합니다"
            )
        return self

# 성공
config = ExperimentConfig(name="실험 1", learning_rate=0.001, batch_size=32)

# 실패: 배치 크기 검증
try:
    bad = ExperimentConfig(name="실험 2", learning_rate=0.001, batch_size=30)
except Exception as e:
    print(f"검증 실패: {e}")

# 실패: 비율 합산 검증
try:
    bad2 = ExperimentConfig(
        name="실험 3",
        learning_rate=0.001,
        batch_size=32,
        train_ratio=0.7,
        val_ratio=0.2,
    )
except Exception as e:
    print(f"검증 실패: {e}")

BaseSettings로 환경 변수 관리

from pydantic_settings import BaseSettings
from pydantic import Field

class AppSettings(BaseSettings):
    """환경 변수에서 자동으로 설정을 로딩합니다."""

    # 환경 변수 API_KEY → api_key
    api_key: str = Field(description="API 키")
    model_name: str = Field(default="gpt-4")
    max_tokens: int = Field(default=4096, ge=1)
    temperature: float = Field(default=0.7, ge=0, le=2)
    debug: bool = Field(default=False)

    model_config = {
        "env_prefix": "",           # 환경 변수 접두사 (없음)
        "env_file": ".env",          # .env 파일에서 로딩
        "env_file_encoding": "utf-8",
        "case_sensitive": False,     # 대소문자 무시
    }

# .env 파일 예시:
# API_KEY=sk-xxx
# MODEL_NAME=gpt-4o
# DEBUG=true

# 환경 변수 또는 .env 파일에서 자동 로딩
# settings = AppSettings()
# print(settings.api_key)
# print(settings.model_name)
BaseSettings는 환경 변수 > .env 파일 > 기본값 순서로 우선순위를 적용합니다. 프로덕션에서는 환경 변수로, 개발에서는 .env 파일로 설정을 관리하는 패턴이 일반적입니다.

AI/ML에서의 활용

from pydantic import BaseModel, Field, field_validator, model_validator
from typing import Literal
from pathlib import Path

# 학습 파이프라인 설정
class DataConfig(BaseModel):
    train_path: str
    val_path: str
    test_path: str | None = None
    max_length: int = Field(default=512, ge=1, le=8192)
    num_workers: int = Field(default=4, ge=0)

    @field_validator("train_path", "val_path")
    @classmethod
    def path_must_exist(cls, v: str) -> str:
        if not Path(v).exists():
            raise ValueError(f"경로가 존재하지 않습니다: {v}")
        return v

class ModelConfig(BaseModel):
    name: str = "bert-base-uncased"
    hidden_size: int = Field(default=768, ge=1)
    num_layers: int = Field(default=12, ge=1)
    num_heads: int = Field(default=12, ge=1)
    dropout: float = Field(default=0.1, ge=0.0, lt=1.0)

    @model_validator(mode="after")
    def check_head_divisibility(self) -> "ModelConfig":
        if self.hidden_size % self.num_heads != 0:
            raise ValueError(
                f"hidden_size({self.hidden_size})는 "
                f"num_heads({self.num_heads})로 나누어져야 합니다"
            )
        return self

class TrainPipelineConfig(BaseModel):
    """전체 학습 파이프라인 설정"""
    data: DataConfig
    model: ModelConfig
    optimizer: Literal["adam", "sgd", "adamw"] = "adamw"
    learning_rate: float = Field(default=5e-5, gt=0)
    epochs: int = Field(default=3, ge=1)
    seed: int = 42

# YAML/JSON 설정 파일에서 로딩
import json

def load_pipeline_config(path: str) -> TrainPipelineConfig:
    """JSON 설정 파일을 검증하여 로딩합니다."""
    with open(path, "r", encoding="utf-8") as f:
        raw = json.load(f)
    return TrainPipelineConfig.model_validate(raw)

# config = load_pipeline_config("pipeline_config.json")
# print(config.model.hidden_size)
# print(config.data.max_length)
Pydantic v2는 Rust 기반 코어(pydantic-core)로 5~50배 빨라졌습니다. 주요 API 변경: dict()model_dump(), json()model_dump_json(), parse_obj()model_validate(), validatorfield_validator. 새 프로젝트에서는 반드시 v2를 사용하세요.
dataclass는 데이터 저장 컨테이너이고, Pydantic은 데이터 검증 프레임워크입니다. Pydantic은 자동 타입 변환, 제약 조건 검증, JSON 직렬화를 기본 제공합니다. 외부 입력(API, 파일)을 다룰 때는 Pydantic, 내부 데이터 구조에는 dataclass가 적합합니다.

체크리스트

  • BaseModel로 데이터 모델을 정의하고 검증할 수 있다
  • Field로 숫자 범위, 문자열 길이 등 제약 조건을 설정할 수 있다
  • field_validatormodel_validator로 커스텀 검증을 구현할 수 있다
  • model_dump()model_validate()로 직렬화/역직렬화할 수 있다
  • BaseSettings로 환경 변수 기반 설정을 관리할 수 있다

다음 문서