파이썬 타입 입문

파이썬 타입 입문 (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_namelast_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에 오류가 표시된 스크린샷이 있어요.)

이제 고쳐야 한다는 걸 알죠. agestr(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뿐 아니라 모든 표준 파이썬 타입을 선언할 수 있어요.

예를 들면 이런 것들이죠:

  • int
  • float
  • bool
  • bytes
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 해야 할 수도 있어요. 예를 들어 어떤 값이 "아무 타입"이라는 걸 선언하고 싶다면 typingAny를 쓸 수 있어요:

from typing import Any


def some_function(data: Any):
    print(data)

제네릭 타입 (Generic types)

일부 타입은 내부 타입을 정의하기 위해 **대괄호 안에 "타입 파라미터"**를 받을 수 있어요. 예를 들어 "문자열들의 리스트"는 list[str]로 선언해요.

이렇게 타입 파라미터를 받을 수 있는 타입들을 제네릭 타입(Generic types) 또는 **제네릭(Generics)**이라고 해요.

빌트인 타입도 제네릭으로(대괄호 안에 타입을 넣어서) 쓸 수 있어요:

  • list
  • tuple
  • set
  • dict

List

예를 들어 str들의 list인 변수를 정의해 볼게요.

콜론(:) 문법으로 변수를 선언해요.

타입으로 list를 넣고요.

list는 내부 타입을 포함하는 타입이므로, 그 내부 타입을 대괄호 안에 넣어요:

def process_items(items: list[str]):
    for item in items:
        print(item)

참고

대괄호 안의 그 내부 타입들을 "타입 파라미터"라고 불러요.

여기서는 strlist에 전달된 타입 파라미터입니다.

이건 "변수 itemslist인데, 그 안의 각 항목은 str다"라는 뜻이에요.

이렇게 하면 에디터가 리스트의 항목을 처리하는 동안에도 지원을 제공할 수 있어요. (원문에는 리스트 항목 처리 시 자동완성이 보이는 스크린샷이 있어요.)

타입이 없으면 그건 거의 불가능해요.

변수 item이 리스트 items의 요소 하나라는 걸 눈여겨보세요.

그런데도 에디터는 그것이 str임을 알고 지원을 해 주죠.

Tuple과 Set

tupleset도 같은 방식으로 선언해요:

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_sset이고, 그 안의 각 항목은 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)

이건 이런 뜻이에요:

  • 변수 pricesdict이다:
    • dict의 키는 str 타입이다(각 항목의 이름이라고 해 볼게요).
    • dict의 값은 float 타입이다(각 항목의 가격이라고 해 볼게요).

Union

변수가 여러 타입 중 하나일 수 있다고 선언할 수 있어요. 예를 들어 int 또는 str이요.

정의하려면 수직 막대(|)(비트 OR 연산자라고도 불리지만, 여기선 그 의미는 상관없어요)로 두 타입을 구분해요.

이걸 "union"(유니언)이라 불러요. 그 변수가 두 타입 집합의 합집합 중 무엇이든 될 수 있기 때문이에요.

def process_item(item: int | str):
    print(item)

이건 itemint 또는 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_personPerson이라는 클래스 자체다"라는 뜻이 아니에요.

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"가 좋은 자료예요.

더 알아보기 (Learn more)