파이썬 타입 입문
파이썬 타입 입문 (Python Types Intro)
FastAPI는 전부 파이썬 타입 힌트(type hints) 위에서 만들어졌어요. 그 덕에 에디터 자동완성이라든가 타입 검사 같은 걸 그냥 얻을 수 있죠. 이 글은 그 타입 힌트를 FastAPI에서 쓰는 데 필요한 최소한만 빠르게 정리해 주는 입문/복습 편이에요. 사실 필요한 건 정말 적답니다.
출처: 공식문서
파이썬은 선택적인 "타입 힌트"(type hints, "타입 어노테이션"이라고도 불러요)를 지원해요.
이 **"타입 힌트"**나 어노테이션은 변수의 타입(str, int, float, bool 같은)을 선언할 수 있게 해 주는 특별한 문법입니다.
변수에 타입을 선언해 두면, 에디터와 도구들이 더 나은 지원을 해 줘요.
이건 타입 힌트에 대한 빠른 입문/복습이고, FastAPI와 함께 쓰는 데 필요한 최소한만 다뤄요. ...실제로 아주 적답니다.
FastAPI는 전부 이 타입 힌트에 기반하고, 그 힌트가 많은 장점과 이점을 주죠.
하지만 FastAPI를 쓰지 않더라도 조금 배워 두면 도움이 돼요.
참고
파이썬 전문가라서 타입 힌트를 이미 다 안다면, 다음 장으로 넘어가도 좋아요.
동기 (Motivation)
간단한 예시부터 시작할게요:
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
이 프로그램을 실행하면 이렇게 출력돼요:
John Doe
이 함수는 이런 일을 해요:
first_name과last_name을 받는다.title()로 각각의 첫 글자를 대문자로 바꾼다.- 가운데 공백 하나를 넣어서 이어 붙인다(concatenate).
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
고쳐 보기 (Edit it)
아주 단순한 프로그램이죠.
그런데 이걸 처음부터 새로 짠다고 상상해 볼게요.
어느 순간 함수를 정의하기 시작하고, 파라미터들이 준비됐어요...
그런데 이제 "첫 글자를 대문자로 만드는 그 메서드"를 불러야 해요.
upper였나? uppercase? first_uppercase? capitalize?
그다음엔 옛날 프로그래머의 친구, 에디터 자동완성을 시도해요.
함수의 첫 파라미터 first_name을 입력하고, 점(.)을 찍은 뒤 Ctrl+Space를 눌러 자동완성을 띄워 봐요.
그런데 아쉽게도 쓸 만한 게 하나도 안 나와요. (원문에는 이 상태의 에디터 스크린샷이 있어요.)
타입 추가하기 (Add types)
이전 버전에서 한 줄만 고쳐 볼게요.
함수의 파라미터인 정확히 이 부분을, 이렇게:
first_name, last_name
이렇게 바꿔요:
first_name: str, last_name: str
그게 전부예요.
이게 바로 "타입 힌트"입니다:
def get_full_name(first_name: str, last_name: str):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
이건 다음과 같이 기본값을 선언하는 것과는 달라요:
first_name="john", last_name="doe"
전혀 다른 개념이에요.
우리는 등호(=)가 아니라 콜론(:)을 쓰고 있어요.
그리고 타입 힌트를 추가한다고 해서, 없을 때와 달라지는 게 보통은 없어요.
이제 다시 그 함수를 만드는 중이라고 상상해 볼게요, 이번엔 타입 힌트와 함께요.
같은 지점에서 Ctrl+Space로 자동완성을 띄워 보면, 이제 이렇게 옵션이 보여요. (원문에는 자동완성 옵션들이 보이는 스크린샷이 있어요.)
스크롤을 내려 옵션을 보다가 "알이 딱 맞는" 걸 찾으면 됩니다.
좀 더 생각해 보기 (More motivation)
이 함수를 확인해 볼게요. 이미 타입 힌트가 있어요:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
에디터가 변수의 타입을 알기 때문에, 자동완성뿐 아니라 오류 검사도 받을 수 있어요. (원문에는 age에 오류가 표시된 스크린샷이 있어요.)
이제 고쳐야 한다는 걸 알죠. age를 str(age)로 문자열로 변환해요:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
타입 선언하기
타입 힌트를 선언하는 주요 위치는 방금 본 대로 함수 파라미터예요.
FastAPI에서도 그것이 바로 이 타입 힌트를 쓰는 주요 위치입니다.
단순 타입
str뿐 아니라 모든 표준 파이썬 타입을 선언할 수 있어요.
예를 들면 이런 것들이죠:
intfloatboolbytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_e
typing 모듈
몇 가지 추가 사용 사례에서는 표준 라이브러리 typing 모듈에서 무언가를 import 해야 할 수도 있어요. 예를 들어 어떤 값이 "아무 타입"이라는 걸 선언하고 싶다면 typing의 Any를 쓸 수 있어요:
from typing import Any
def some_function(data: Any):
print(data)
제네릭 타입 (Generic types)
일부 타입은 내부 타입을 정의하기 위해 **대괄호 안에 "타입 파라미터"**를 받을 수 있어요. 예를 들어 "문자열들의 리스트"는 list[str]로 선언해요.
이렇게 타입 파라미터를 받을 수 있는 타입들을 제네릭 타입(Generic types) 또는 **제네릭(Generics)**이라고 해요.
빌트인 타입도 제네릭으로(대괄호 안에 타입을 넣어서) 쓸 수 있어요:
listtuplesetdict
List
예를 들어 str들의 list인 변수를 정의해 볼게요.
콜론(:) 문법으로 변수를 선언해요.
타입으로 list를 넣고요.
list는 내부 타입을 포함하는 타입이므로, 그 내부 타입을 대괄호 안에 넣어요:
def process_items(items: list[str]):
for item in items:
print(item)
참고
대괄호 안의 그 내부 타입들을 "타입 파라미터"라고 불러요.
여기서는
str가list에 전달된 타입 파라미터입니다.
이건 "변수 items는 list인데, 그 안의 각 항목은 str다"라는 뜻이에요.
이렇게 하면 에디터가 리스트의 항목을 처리하는 동안에도 지원을 제공할 수 있어요. (원문에는 리스트 항목 처리 시 자동완성이 보이는 스크린샷이 있어요.)
타입이 없으면 그건 거의 불가능해요.
변수 item이 리스트 items의 요소 하나라는 걸 눈여겨보세요.
그런데도 에디터는 그것이 str임을 알고 지원을 해 주죠.
Tuple과 Set
tuple과 set도 같은 방식으로 선언해요:
def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
return items_t, items_s
이건 이런 뜻이에요:
- 변수
items_t는 항목 3개(int,int,str)를 가진tuple이다. - 변수
items_s는set이고, 그 안의 각 항목은bytes타입이다.
Dict
dict를 정의하려면 콤마로 구분된 타입 파라미터 2개를 전달해요.
첫 번째 타입 파라미터는 dict의 키용이에요.
두 번째 타입 파라미터는 dict의 값용이고요:
def process_items(prices: dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
이건 이런 뜻이에요:
- 변수
prices는dict이다:- 이
dict의 키는str타입이다(각 항목의 이름이라고 해 볼게요). - 이
dict의 값은float타입이다(각 항목의 가격이라고 해 볼게요).
- 이
Union
변수가 여러 타입 중 하나일 수 있다고 선언할 수 있어요. 예를 들어 int 또는 str이요.
정의하려면 수직 막대(|)(비트 OR 연산자라고도 불리지만, 여기선 그 의미는 상관없어요)로 두 타입을 구분해요.
이걸 "union"(유니언)이라 불러요. 그 변수가 두 타입 집합의 합집합 중 무엇이든 될 수 있기 때문이에요.
def process_item(item: int | str):
print(item)
이건 item이 int 또는 str일 수 있다는 뜻이에요.
None일 수도 있을 때
값이 str 같은 어떤 타입일 수 있지만, None일 수도 있다고 선언할 수 있어요.
def say_hi(name: str | None = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
str 대신 str | None을 쓰면, 에디터가 "값이 항상 str이라고 가정하는데 실제로 None일 수도 있다"는 오류를 잡아내는 걸 도와줘요.
클래스를 타입으로
변수의 타입으로 클래스를 선언할 수도 있어요.
name을 가진 Person 클래스가 있다고 해 볼게요:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
그런 다음 변수를 Person 타입으로 선언할 수 있어요:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
그리고 다시, 모든 에디터 지원을 받을 수 있어요. (원문에는 Person 인스턴스에서 자동완성이 보이는 스크린샷이 있어요.)
이건 "one_person은 클래스 Person의 인스턴스다"라는 뜻이에요.
"one_person이 Person이라는 클래스 자체다"라는 뜻이 아니에요.
Pydantic 모델
Pydantic은 데이터 검증(data validation)을 수행하는 파이썬 라이브러리예요.
데이터의 "모양(shape)"을 속성을 가진 클래스로 선언해요.
그리고 각 속성은 타입을 가집니다.
그리고 몇 가지 값으로 그 클래스의 인스턴스를 만들면, 값들을 검증하고, 해당하는 경우 적절한 타입으로 변환한 다음, 모든 데이터를 가진 객체를 돌려줘요.
결과적으로 얻는 객체에서도 모든 에디터 지원을 받을 수 있어요.
공식 Pydantic 문서의 예시를 가져와 볼게요:
from datetime import datetime
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: datetime | None = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
참고
Pydantic에 대해 더 배우고 싶다면, 공식 문서(https://pydantic.dev/docs/)를 확인하세요.
FastAPI는 전부 Pydantic에 기반해요.
이 모든 것의 실제 사용은 Tutorial - User Guide에서 훨씬 더 많이 보게 될 거예요.
메타데이터 어노테이션이 있는 타입 힌트 (Annotated)
파이썬에는 Annotated를 써서 타입 힌트에 추가 메타데이터(데이터에 관한 데이터, 여기서는 타입에 관한 정보, 예: 설명)를 넣을 수 있는 기능도 있어요.
typing에서 Annotated를 import 할 수 있어요.
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
파이썬 자체는 이 Annotated로 아무것도 하지 않아요. 그리고 에디터나 다른 도구 입장에서 타입은 여전히 str이에요.
하지만 이 Annotated 안의 공간을 사용해서 FastAPI에 "애플리케이션이 어떻게 동작하길 원하는지"에 대한 추가 메타데이터를 제공할 수 있어요.
기억할 중요한 점은 Annotated에 전달하는 첫 번째 타입 파라미터가 실제 타입이라는 거예요. 나머지는 그저 다른 도구들을 위한 메타데이터일 뿐이죠.
지금으로선 Annotated가 존재하고, 표준 파이썬이라는 것만 알면 돼요. 😎
나중에 그것이 얼마나 강력한지 보게 될 거예요.
팁
이것이 표준 파이썬이라는 사실은, 에디터에서 코드를 분석하고 리팩토링하는 도구들로 최고의 개발자 경험을 여전히 얻을 수 있다는 뜻이에요. ✨
그리고 코드도 다른 많은 파이썬 도구·라이브러리와 아주 잘 호환돼요. 🚀
FastAPI에서의 타입 힌트
FastAPI는 이 타입 힌트를 활용해서 여러 가지 일을 해요.
FastAPI로 타입 힌트가 있는 파라미터를 선언하면 다음을 얻어요:
- 에디터 지원(Editor support).
- 타입 검사(Type checks).
...그리고 FastAPI는 같은 선언을 써서 이런 일을 해요:
- 요구사항 정의: 요청의 path 파라미터, 쿼리 파라미터, 헤더, 본문(body), 의존성 등에서.
- 데이터 변환: 요청에서 필요한 타입으로.
- 데이터 검증: 각 요청에서 오는 데이터를:
- 데이터가 유효하지 않으면 클라이언트에게 돌려주는 자동 오류 생성.
- OpenAPI로 API 문서화:
- 이는 자동 대화형 문서 UI들이 사용하게 돼요.
이게 다 추상적으로 들릴 수도 있는데, 걱정 마세요. Tutorial - User Guide에서 이 모든 것을 실제로 보게 될 거예요.
중요한 건 표준 파이썬 타입을 한 곳에서(클래스·데코레이터를 더 추가하는 대신) 씀으로써 FastAPI가 많은 일을 대신 해 준다는 점이에요.
참고
튜토리얼을 이미 다 돌리고 타입에 대해 더 알아보려고 돌아왔다면,
mypy의 "cheat sheet"가 좋은 자료예요.