결론: 신뢰할 수 없는 입력은 타입 힌트가 아니라 Pydantic으로 실행 중에 검증한다
지난 글의 타입 힌트는 개발 중 도구가 오류를 미리 찾도록 돕지만, 프로그램이 실제로 실행되는 동안에는 아무것도 강제하지 않는다. CSV 행, API 응답, 사용자 입력처럼 코드 밖에서 들어오는 데이터는 타입이 맞다는 보장이 없으므로 실행 시점에 검증하는 절차가 따로 필요하다. Pydantic은 BaseModel 클래스에 타입 힌트로 스키마를 선언하면, 그 힌트를 실제 검증·형변환 규칙으로 실행해 어긋난 값을 ValidationError로 걸러 준다.
BaseModel: 타입 힌트를 실행되는 스키마로
pydantic.BaseModel을 상속한 클래스는 클래스 속성에 적은 타입 힌트를 실제 검증 규칙으로 사용한다. 인스턴스를 만드는 순간 모든 필드가 검증되고, 필요하면 타입을 자동으로 변환한다.
from typing import Optional
from pydantic import BaseModel, Field
class SalesRecord(BaseModel):
date: str
region: str
amount: float = Field(gt=0, description="양수 금액")
category: Optional[str] = None
record = SalesRecord(**{"date": "2026-08-10", "region": "서울", "amount": 1500})
print(record)
print(record.model_dump())
print(record.model_dump_json())
date='2026-08-10' region='서울' amount=1500.0 category=None
{'date': '2026-08-10', 'region': '서울', 'amount': 1500.0, 'category': None}
{"date":"2026-08-10","region":"서울","amount":1500.0,"category":null}
amount에 정수 1500을 넘겼지만 힌트가 float이므로 1500.0으로 자동 변환됐다. model_dump()는 검증된 값을 평범한 dict로, model_dump_json()은 JSON 문자열로 직렬화한다. 이 둘은 이전 시리즈에서 다룬 json.dump와 달리 모델이 가진 타입 정보를 그대로 활용해 변환한다.
Field: 범위와 설명을 더한 정밀한 제약
Field(...)는 타입만으로 표현할 수 없는 제약을 추가한다. gt·ge·lt·le로 숫자 범위를, description으로 필드 설명을 붙인다. 값 하나가 규칙을 어기면 그 필드만 콕 집어 실패 이유를 알려준다.
try:
SalesRecord(date="", region="서울", amount=-100)
except Exception as exc:
print(exc)
1 validation error for SalesRecord
amount
Input should be greater than 0 [type=greater_than, input_value=-100, input_type=int]
For further information visit https://errors.pydantic.dev/2.13/v/greater_than
ValidationError는 어느 필드가, 어떤 값 때문에, 왜 실패했는지까지 한 번에 알려준다. 이 메시지를 문자열로 파싱하는 대신, 아래에서 다룰 .errors()로 구조화된 형태로 받아 쓴다.
타입 변환의 범위: 무엇을 자동으로 바꿔 주는가
Pydantic v2는 명확하게 변환 가능한 값은 자동으로 바꿔 준다. 문자열 "1"은 int 필드에 넣으면 1로 바뀌지만, "abc"처럼 숫자로 해석할 수 없는 문자열은 변환하지 않고 오류로 남긴다. 이 성질 덕분에 문자열만 주는 CSV 데이터를 Pydantic 모델에 그대로 넘겨도 숫자 필드가 알맞게 변환된다.
from pydantic import BaseModel, ValidationError
class Record(BaseModel):
id: int
product: str
amount: float
region: str
# CSV에서 읽은 값은 전부 문자열이지만 int/float 필드로 자동 변환된다
rec = Record.model_validate({"id": "1", "product": "노트북", "amount": "1250000", "region": "서울"})
print(rec)
try:
Record.model_validate({"id": "x", "product": "마우스", "amount": "abc", "region": "인천"})
except ValidationError as exc:
for err in exc.errors():
print(err["loc"], err["msg"])
id=1 product='노트북' amount=1250000.0 region='서울'
('id',) Input should be a valid integer, unable to parse string as an integer
('amount',) Input should be a valid number, unable to parse string as a number
model_validate(딕셔너리)는 **딕셔너리로 필드를 펼쳐 넘기는 것과 같은 결과를 만들지만, CSV·JSON에서 읽은 딕셔너리를 그대로 넘길 수 있어 더 자연스럽다. exc.errors()는 각 오류를 loc(필드 경로)와 msg(이유)를 가진 딕셔너리 리스트로 돌려줘, 오류를 코드로 다시 가공하기 쉽다.
실습: CSV 전체를 검증해 유효/오류로 나누기
행 단위로 검증하면서 성공한 레코드와 실패한 행을 각각 모아 두면, 전체 파이프라인이 첫 오류에서 멈추지 않고 끝까지 처리한 뒤 결과를 한 번에 보고할 수 있다.
import csv
from pydantic import BaseModel, ValidationError
class Record(BaseModel):
id: int
product: str
amount: float
region: str
valid = []
errors = []
with open("data/sales.csv", encoding="utf-8", newline="") as f:
for i, row in enumerate(csv.DictReader(f)):
try:
valid.append(Record.model_validate(row))
except ValidationError as exc:
errors.append({"row": i, "error": str(exc)})
print(f"유효: {len(valid)}건, 오류: {len(errors)}건")
for rec in valid:
print(rec)
유효: 3건, 오류: 0건
id=1 product='노트북' amount=1250000.0 region='서울'
id=2 product='키보드' amount=89000.0 region='부산'
id=3 product='모니터' amount=310000.0 region='서울'
오류 행을 건너뛸지, 파이프라인 전체를 멈출지는 데이터 성격에 달려 있다. 결측·오타가 흔한 외부 수집 데이터는 오류를 모아 리포트로 남기고 유효한 행만 계속 처리하는 편이 실무적이며, 재무처럼 한 건의 오류도 허용할 수 없는 데이터는 첫 오류에서 즉시 중단하는 편이 안전하다.
model_json_schema: 검증 규칙을 문서로 꺼내기
model_json_schema()는 모델의 필드·타입·제약을 JSON Schema 형식으로 돌려준다. API 문서를 자동 생성하는 FastAPI 같은 프레임워크가 바로 이 스키마를 사용하며, Pydantic 모델 하나가 검증 코드이자 문서이자 API 계약서 역할을 겸한다.
import json
print(json.dumps(SalesRecord.model_json_schema(), ensure_ascii=False, indent=2))
{
"properties": {
"date": {"title": "Date", "type": "string"},
"region": {"title": "Region", "type": "string"},
"amount": {
"description": "양수 금액",
"exclusiveMinimum": 0,
"title": "Amount",
"type": "number"
},
"category": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": null,
"title": "Category"
}
},
"required": ["date", "region", "amount"],
"title": "SalesRecord",
"type": "object"
}
타입 시스템이 분석 코드에 주는 이득
이득 | 설명 |
|---|---|
오타 조기 발견 | amount와 Amount 같은 컬럼명 혼동을 수백만 행을 처리한 뒤가 아니라 첫 검증에서 잡는다 |
팀 협업 계약 | 함수·모델의 필드와 타입이 곧 문서가 되어 별도 설명 없이도 인터페이스가 분명해진다 |
자동완성 품질 | 정확한 타입 정보로 에디터가 더 정밀한 자동완성과 오류 표시를 제공한다 |
다음 단계 연결 | FastAPI 같은 웹 프레임워크가 같은 BaseModel을 요청·응답 스키마로 그대로 재사용한다 |
자주 발생하는 문제
필드에 기본값을 주지 않으면 그 필드는 필수가 된다. 값이 없을 수 있는 필드는
Optional[X] = None처럼 기본값을 함께 지정한다.SalesRecord(**row)처럼 딕셔너리를 직접 펼치면 예상치 못한 여분의 키가 있을 때 동작이 설정에 따라 달라질 수 있다. 외부 데이터를 다룰 때는 의미가 분명한model_validate(row)를 우선한다.Pydantic의 자동 형변환을 만능이라 여기면 안 된다.
"abc"처럼 숫자로 해석할 수 없는 문자열은 여전히 오류가 나며, 이는 의도된 동작이다.ValidationError메시지를 로그 없이 그냥 삼키면 어떤 행이 왜 실패했는지 나중에 알 수 없다.exc.errors()로 구조화해 로그나 오류 리포트 파일로 남긴다.모델 필드 타입을 실제 데이터보다 느슨하게(예: 전부 str) 잡으면 검증이 사실상 통과만 시키는 형식적 절차가 된다. 실제 제약(범위, 필수 여부)을 필드에 반영해야 검증의 의미가 생긴다.
다음 글: pytest로 데이터 분석 함수를 테스트하기
Pydantic이 데이터의 형태를 검증한다면, 다음 글의 pytest는 그 데이터를 다루는 함수의 동작 자체를 검증한다. fixture로 테스트 데이터를 준비하고, 이번 글의 ValidationError처럼 예외가 발생해야 하는 경우를 테스트로 명시하는 방법을 다룬다.
참고 자료
Pydantic 공식 문서: Models — BaseModel 정의와 model_dump·model_validate (2026-08-10 확인)
Pydantic 공식 문서: Fields — Field의 gt/lt/ge/le와 제약 옵션 (2026-08-10 확인)
Pydantic 공식 문서: Error Handling — ValidationError 구조와 errors() (2026-08-10 확인)
Pydantic 공식 문서: JSON Schema — model_json_schema로 스키마 내보내기 (2026-08-10 확인)
댓글 0
댓글을 불러오는 중…