FastAPI 개발을 위한 Python 타입 힌트 알아보기
목차
FastAPI에서의 타입 힌트
FastAPI에서는 Python의 Type Hint를 사용합니다. 모든 코드에 강제되는 것은 아니지만 FastAPI를 잘 쓰기 위해서는 어느 정도 필요합니다. APIRouter
와 관련된 코드에서 타입 힌트를 사용하면 API에서 받는 입력 값들에 대해 유효성 검사가 이루어 집니다. 이번 포스팅에서 FastAPI 개발을 위해서 필요한 Python의 타입 힌트에 대해서 알아보겠습니다. Python 3.10 버전 이상을 사용한다고 가정하고 작성합니다.
FastAPI에서는 엔드포인트를 구현하는 APIRouter
에서 타입 힌트를 다음과 같이 사용하는데 몇 가지 특징이 있습니다.
from fastapi import FastAPI
app = FastAPI()
def get_response(param: str):
return {"message": f"Hello {param}"}
@app.get("/")
def example_route(example_param: str):
return get_response(example_param)
- 라우팅하는 데코레이터가 붙은 함수에서만 타입 힌트로 유효성 검사가 이루어 집니다.
- 그 외의 함수에서는 예시의
get_response
처럼 해도 유효성 검사가 이루어 지지 않습니다. - FastAPI에서는 반환 값에 대한 타입 힌트는 유효성 검사가 따로 없기 때문에 써도 되고 안써도 됩니다.
FastAPI에서 유효성 검사를 위해 Pydantic을 사용합니다. 위에 사용된 예시 코드는 Query Parameter를 사용할 때 예시입니다. POST 요청에서는 Request Body에 대한 유효성 검사를 할 때 Pydantic을 많이 사용합니다.
실제 사용할 만한 예시를 들어보겠습니다.
from pydantic import BaseModel, EmailStr, model_validator, field_validator
class RegisterUserRequest(BaseModel):
email: EmailStr | None = None
password: str | None = None
password_confirmation: str = None
@field_validator("password")
@classmethod
def validate(cls, value):
if len(value) < 8:
raise ValueError("비밀번호는 8자 이상이어야 합니다.")
if not any(char.isdigit() for char in value):
raise ValueError("비밀번호는 숫자를 포함해야 합니다.")
if not any(char.isalpha() for char in value):
raise ValueError("비밀번호는 문자를 포함해야 합니다.")
if not any(char.isupper() for char in value):
raise ValueError("비밀번호는 대문자를 포함해야 합니다.")
return value
@model_validator(mode="before")
@classmethod
def validate_password(cls, data):
password = data.get("password")
password_confirmation = data.get("password_confirmation")
if password != password_confirmation:
raise ValueError("비밀번호 확인이 일치하지 않습니다.")
return data
# 다음과 같은 방식으로 Pydantic 모델을 사용할 수 있음
async def check_email_exists(data: Annotated[RegisterUserRequest, Form()], db: DB) -> RegisterUserRequest:
user = await user_orm.get_by_filters(db, columns=["email"], email=data.email)
if user:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="이미 존재하는 이메일입니다.", code="EMAIL_EXISTS")
return data
async def register_user_by_email(vaild_data: Annotated[RegisterUserRequest, Depends(check_email_exists)], db: DB) -> dict:
data = vaild_data.model_dump()
password = PasswordService.get_password_hash(data.get("password"))
data = {
"email": data.get("email"),
"password": password,
}
user = await user_orm.create(db, **data)
result = {
"id": user.id,
"email": user.email,
}
return result
Pydantic을 이용한 Query Parameter 만드는 방법은 아래 링크에서 확인할 수 있습니다.
Python 기본 데이터 타입 힌트
타입 힌트는 Python 3.5부터 도입된 기능입니다. 변수, 함수의 매개변수, 반환값 등에 타입을 명시적으로 선언할 수 있습니다. 명시적으로 선언한다고 해서 강제가 아니고 개발의 편의성을 위해서 타입을 명시하는 것이기 때문에 힌트라고 합니다. 가독성이 좀 더 좋아지고 mypy같은 툴로 분석할 수도 있습니다.
Python에 존재하는 기본 데이터 타입을 타입 힌트로 사용할 수 있습니다. str
, int
, float
, list
, tuple
, dict
등을 하나만 사용할 수도 있고 결합해서 사용할 수도 있습니다.
개별적으로 사용하는 방법은 다음과 같습니다.
def add(param_1: str, param_2: str) -> str:
return param_1 + param_2
- 함수에서 사용할 때 파라미터에
:
을 사용합니다. - 반환을 표시할 때는
→
를 사용합니다.
함수에서 하나의 파라미터에 타입이 여러가지 들어갈 수도 있습니다. 그런 경우 다음과 같이 사용합니다.
def add2(param_1: str | int, param_2: str | int) -> str | int:
return param_1 + param_2
|
를 사용해서or
를 표현할 수 있습니다. 위 예시에서 파라미터는str
또는int
타입을 받는 다는 뜻입니다.
만약에 리스트, 튜플, 딕셔너리를 사용하는 경우 내부 속성의 타입 힌트도 지정할 수 있습니다. 다음과 같이 타입들을 결합해서 사용하는 것이 가능합니다.
def example_function(
param_1: list[str], # 문자열 리스트
param_2: tuple[int, float], # 정수와 실수로 이루어진 튜플
param_3: dict[str, int] # 문자열 키와 정수 값을 가진 딕셔너리
):
...
- 리스트, 튜플, 딕셔너리의 내부 타입을 대괄호([])를 사용해서 지정할 수 있습니다.
- 튜플의 경우 각 위치별로 타입을 다르게 지정할 수 있고, 딕셔너리는 키와 값의 타입을 각각 지정할 수 있습니다.
Python 기본 데이터 타입 이외의 타입 힌트
Python에 typing
내장 라이브러리를 이용해서 기본 데이터 타입 이외의 타입 힌트를 사용할 수 있습니다. 그리고 사용자 정의 클래스를 타입 힌트로 지정할 수도 있습니다.
예를 들면 typing
라이브러리의 Optional
, Union
, Any
, Callable
, Iterable
, TypeVar
, Generic
등의 타입을 사용하거나, 사용자 정의 클래스를 타입 힌트로 활용할 수 있습니다. 몇 가지 예시를 살펴보겠습니다. 대부분 단언 뜻 그대로 이해하면 됩니다.
Callable
과Iterable
예시
from typing import Callable, Iterable
# 숫자 리스트에서 필터 조건을 만족하는 숫자만 추출
def filter_numbers(numbers: Iterable[int], condition: Callable[[int], bool]) -> Iterable[int]:
"""
숫자 Iterable과 필터 조건을 받아 조건을 만족하는 숫자만 반환합니다.
:param numbers: 숫자가 포함된 Iterable
:param condition: 숫자를 입력으로 받아 True/False를 반환하는 함수
:return: 조건을 만족하는 숫자 Iterable
"""
return (num for num in numbers if condition(num))
# 문자열을 받아 각 문자열의 길이를 반환
def get_string_lengths(strings: Iterable[str]) -> Iterable[int]:
"""
문자열 Iterable을 받아 각 문자열의 길이를 계산합니다.
:param strings: 문자열이 포함된 Iterable
:return: 각 문자열 길이의 Iterable
"""
return (len(s) for s in strings)
Callable[[int], bool]
를 해석하면 Callable은 호출 가능한 객체를 의미합니다. 일반적으로 함수를 생각해볼 수 있습니다.[int]
는 함수의 파라미터의 타입입니다.bool
은 함수의 반환 타입입니다.Iterable[str]
은 내부 원소로 문자열을 가지고 있는 순회가능한 객체를 의미합니다. 예를들면[”a”, “b”, “c”]
,("a”, “b”, “c”)
이런 리스트나 튜플이 있습니다.
TypeVar
예시
from typing import TypeVar, Callable
T = TypeVar('T') # 입력 타입
R = TypeVar('R') # 출력 타입
def transform(items: list[T], transformer: Callable[[T], R]) -> list[R]:
"""
리스트의 각 아이템을 주어진 변환 함수를 이용해 변환합니다.
:param items: 변환할 원본 리스트
:param transformer: 변환 함수 (입력 타입 T -> 출력 타입 R)
:return: 변환된 리스트
"""
return [transformer(item) for item in items]
T
는 관습적으로 Type을 나타내며 입력 타입을 의미합니다. 입력 타입이 특정하게 정해져 있지 않을 때 많이 사용합니다.list[T]
는 리스트의 원소의 타입이 특정하게 정해져 있지 않다는 뜻입니다.list[R]
는 마찬 가지로 반환되는 리스트의 원소의 타입이 정해져 있지 않다는 뜻입니다.
typing
라이브러리를 이용한 타입힌트는 언급한 것 이외에도 많이 있습니다. 추가적으로 궁금하다면 더 찾아보시길 바랍니다.
- 사용자 정의 클래스 예시
from math import sqrt
class Point:
"""
2차원 좌표를 나타내는 클래스
"""
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def distance(point1: Point, point2: Point) -> float:
"""
두 점 사이의 거리를 계산합니다.
:param point1: 첫 번째 점
:param point2: 두 번째 점
:return: 두 점 사이의 거리
"""
return sqrt((point2.x - point1.x) ** 2 + (point2.y - point1.y) ** 2)
Point
라는 좌표를 나타내는 사용자 정의 클래스를distance
함수의 파라미터의 타입으로 사용하고 있습니다.
Annotated 이해하기
FastAPI 공식 문서를 보면 타입 힌트를 쓸 때 Annotated
를 사용하고 있는 것을 볼 수 있습니다. Annotated
는 타입 힌트에 메타데이터를 추가하는 용도로 쓰입니다. 의존성 주입에도 활용되고 있기 때문에 Annotated
를 잘 알고 있는 것이 중요합니다.
from typing import Annotated
# Annotated[기본 타입, 메타데이터]
# 처음 예시 코드
data: Annotated[RegisterUserRequest, Form()]
# 변수에 할당해서 재활용 가능
DB = Annotated[AsyncSession, Depends(get_db)]
# FastAPI에서 이런 방식으로 의존성 주입 가능
db: DB
data
파라미터는RegisterUserRequest
타입인데 그것은Form
데이터, 라고 해석할 수 있게 됩니다.Annotated
를 이용해서 의존성을 유연하게 관리할 수 있고 재활용하기도 쉬워집니다.
마무리
FastAPI에서 타입힌트를 사용하기 때문에 FastAPI 개발을 위한 Python의 타입 힌트를 조금 알아봤습니다. Python으로 처음 개발을 시작한 개발자에게는 타입 힌트가 익숙하지 않지만 FastAPI를 쓰려면 어느 정도는 알아야 합니다.
타입 힌트를 사용하면 코드의 의도를 명확하게 표현할 수 있고, IDE의 자동 완성 기능을 더 잘 활용할 수도 있습니다. 또한 런타임 이전에 타입 관련 오류를 미리 발견할 수 있어 코드의 안정성을 높일 수 있는 등, 여러 가지 장점도 많습니다. 물론 타입을 원래 안쓰던 Python 개발자에게 귀찮음을 더해주는 단점도 있지만요. FastAPI를 쓸 때 말고는 상황에 맞에 알아서 사용하면 되겠습니다.