Python typing

LshDevLog·2026년 5월 5일

python

목록 보기
16/16
post-thumbnail
  • typing
    • Python 코드에 타입 힌트를 작성하기 위해 사용하는 모듈
    • 타입 힌트
      • 실행 중 타입을 강제로 제한하지는 않고 IDE, 타입 검사기, 린터 등이 활용함
      • 코드 가독성, IDE 자동완성, 정적 타입 검사, FastAPI/Pydantic의 데이터 검증 및 자동화 문서에 활용

Union

from typing import Union

def print_id(value: Union[int, str]):
    print(value)
  • 여러 타입 중 하나를 허용할 때 사용
  • Union[int, str]
    • int 또는 str 허용
    • 타입 2개 이상 지정할 수 있음
    • Python 3.10부터 value: int | str 로도 가능


Literal

from typing import Literal

def set_mode(mode: Literal["read", "write"]) -> None:
    ...

set_mode("read") # OK
set_mode("write") # OK
set_mode("delete") # 타입 검사 기준으로 오류
  • 허용할 값을 구체적으로 제한할 때 사용
  • 런타임 강제가 아님
  • Union
    • 여러 타입 중 하나 선택
  • Literal
    • 여러 값 중 하나 선택

enum vs Literal

from enum import Enum

class Mode(str, Enum):
    read = "read"
    write = "write"

def set_mode(mode: Mode) -> None:
    ...

set_mode(Mode.read)
set_mode(Mode.write)
  • Literal은 간단한 값 제한
  • 값 목록이 여러 곳에서 재사용된다면 Enum이 더 적절


Optional

from typing import Optional

def print_name(name: Optional[str] = None): # Union[str, None]과 같음
    print(name)
  • 값이 없을 수도 있을 때 사용
  • Optional[str]str 혹은 None을 허용한다는 뜻
    • Python 3.10부터 name: str | None 로도 가능
  • Optional이 인자를 생략 가능하게 하는건 아님
    • 인자를 생략 가능하게 하는건 기본값 = None


Any

from typing import Any

data: Any

data = "hello"
data = [1,2,3]

print(data)
  • 아무 타입이나 허용
  • 타입 체크가 느슨해지므로 남발하면 좋지 않음


Callable

from typing import Callable


def add(a: int, b: int) -> float:
    return float(a + b)


def run_method(func: Callable[[int, int], float]):
    print(func(1,2))


run_method(add)
3.0
  • 함수를 타입으로 표현
  • Callable[[인자 타입들], 반환 타입]


TypedDict

from typing import TypedDict


class User(TypedDict):
    name: str
    age: int


user: User = {
    "name": "Kim",
    "age": 20
}
  • TypedDict는 dict의 구조를 타입 힌트로 표현할 때 사용
  • 주로 JSON 형태 데이터 구조나 외부 API 응답 구조를 표현할 때 유용
  • 런타임 데이터 검증용이라기보다는 정적 타입 검사 목적에 가까움
  • FastAPI 요청/응답에서는 보통 Pydantic의 BaseModel을 더 많이 사용


TypeVar

기본

from typing import TypeVar

T = TypeVar("T")

def get_first(items: list[T]) -> T:
    return items[0]
  • TypeVar는 타입을 변수처럼 사용할 수 있게 해줌
  • T = TypeVar("T")
    • T는 아직 구체적으로 정해지지 않은 타입 변수
    • "T"는 타입 변수의 이름
    • 보통 대입하는 변수명 T 와 문자열 "T"를 같게 맞춰서 씀
  • get_first(items: list[T]) -> T
    • itemsT 타입 값을 담은 리스트
    • 반환값도 같은 T 타입
    • list[int]를 넣으면 반환 타입은 int
    • list[str]를 넣으면 반환 타입은 str

제약 조건걸기

T = TypeVar("T", int, str)
  • T = TypeVar("T", int, str)
    • Tint 혹은 str 중 하나로 제한하는 제약 조건

Python 3.12+

def get_first[T](items: list[T]) -> T:
    return items[0]
  • Python 3.12 이상부터는 T = TypeVar("T")를 따로 만들지 않아도 됨


Generic

기본

from typing import TypeVar, Generic

T = TypeVar("T")

class Box(Generic[T]): # T라는 타입 변수를 사용하는 제네릭 클래스라는 뜻
    def __init__(self, value: T): # 들어가는 값의 타입 T
        self.value = value

    def get_value(self) -> T: # 나가는 값의 타입도 T
        return self.value
  • TypeVar
    • 타입 변수를 만드는 도구
    • 함수에서는 Generic없이 TypeVar만으로 입력 타입과 반환 타입의 관계를 표현 가능
  • Generic
    • 타입 변수를 클래스에 적용하는 도구
    • 직접 만든 클래스도 list[int], dict[str, int]처럼 타입을 넣어 사용할 수 있게 해줌

Python 3.12+

class Box[T]:
    def __init__(self, value: T):
        self.value = value

    def get_value(self) -> T:
        return self.value
  • Python 3.12버전 이상부터는 GenericTypeVar를 직접 선언하지 않고 타입 파라미터 문법으로 작성 가능


Protocol

기본

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None:
        ...
  • Protocol은 특정 메서드나 속성을 가진 객체를 구조적으로 판단하기 위한 기능
  • Protocol은 Duck Typing 스타일을 타입 힌트로 표현
    • Duck Typing
      • Python은 Duck Typing이라는 문화를 가진 언어
      • 오리처럼 걷고 오리처럼 울면 클래스에 상관없이 오리로 취급한다
  • 위 예제
    • Drawable타입으로 인정받으려면 draw 메서드를 가지고 있어야함

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None:
        ...


class Circle:
    def draw(self) -> None:
        print("원을 그림")


class Rectangle:
    def draw(self) -> None:
        print("사각형을 그림")


def render(obj: Drawable) -> None:
    obj.draw()


render(Circle())
render(Rectangle())
원을 그림
사각형을 그림
  • CircleRectangle 클래스는 Drawable을 상속받지 않았지만 draw 메서드가 있어 타입검사기에서도 문제없음

from typing import Protocol

class HasName(Protocol):
    name: str



class User:
    def __init__(self, name: str):
        self.name = name


def print_name(obj: HasName) -> None:
    print(obj.name)


print_name(User("Kim"))
Kim
  • 속성도 정의 가능

ABC vs Protocol 비교

  • ABC: 이 인터페이스를 상속했는가?
    • class A(ABC) 상속
    • 런타임 추상 클래스, 강제 구현
    • OOP 인터페이스
  • Protocol: 필요한 메서드와 속성을 가지고 있는가?
    • class A(Protocol) 정의
    • 상속하지 않아도 구조가 같으면 됨
    • 정적 타입 검사에서 구조 표현
    • Duck Typing에 가까움

runtime_checkable

from typing import Protocol, runtime_checkable

@runtime_checkable
class HasName(Protocol):
    name: str


class User:
    def __init__(self, name: str):
        self.name = name


print(isinstance(User("kim"), HasName))
True
  • runtime_checkable
    • Protocol은 정적 타입 검사기용
    • Protocolisinstance()에 사용 불가
    • runtime_checkableProtocolisinstance()를 사용할 수 있게 해줌
    • 런타임에서 메서드와 속성의 존재 여부를 확인
    • 속성 타입, 메서드 인자 타입, 반환 타입까지 완벽하게 검증하지는 않음


Annotated

기본

from typing import Annotated

Age = Annotated[int, "0 이상의 정수여야함"]

def print_age(age: Age) -> None:
    print(age)
  • Annotated는 기존 타입 힌트에 추가 정보(메타데이터)를 붙일 때 사용
  • Python이 메타데이터를 자동으로 검사하지는 않음
  • 실제 메타데이터 검증은 해석 도구나 라이브러리가 있어야 가능

FastAPI에서

from typing import Annotated
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items")
def read_items(q: Annotated[str | None, Query(max_length=50)] = None):
    return {"q": q}
  • FastAPI에서 Annotated 자주 쓰임
  • FastAPI에서 Query, Path, Body, Depends와 같은 FastAPI 정보를 타입에 붙일 때 사용

0개의 댓글