본문으로 건너뛰기

타입 힌팅

타입 힌트란?

Python은 동적 타입 언어지만, 타입 힌트를 통해 코드의 의도를 명확히 할 수 있습니다.

# 타입 힌트 없이
def greet(name):
return f"Hello, {name}!"

# 타입 힌트 사용
def greet(name: str) -> str:
return f"Hello, {name}!"

# 장점
# 1. 코드 가독성 향상
# 2. IDE 자동완성 지원
# 3. 정적 분석 도구 활용
# 4. 버그 조기 발견

# ⚠️ 주의: 런타임에는 검사 안 됨!
result = greet(123) # 에러 없이 실행됨
print(result) # Hello, 123!

기본 타입 힌트

내장 타입

# 숫자
def add(a: int, b: int) -> int:
return a + b

def divide(a: float, b: float) -> float:
return a / b

# 문자열
def format_name(first: str, last: str) -> str:
return f"{first} {last}"

# 불린
def is_adult(age: int) -> bool:
return age >= 18

# 바이트
def encode_text(text: str) -> bytes:
return text.encode('utf-8')

# 사용
result = add(3, 5) # OK
# result = add("3", "5") # mypy 경고!

print(is_adult(20)) # True

변수 타입 힌트

# 변수 타입 명시
name: str = "홍길동"
age: int = 25
height: float = 175.5
is_student: bool = True

# 초기값 없이
count: int
count = 0

# 여러 타입 가능 (Union)
from typing import Union

user_id: Union[int, str] = 123
user_id = "user_123" # OK

# Python 3.10+ (더 간단)
user_id: int | str = 123

컬렉션 타입

List, Tuple, Dict, Set

from typing import List, Tuple, Dict, Set

# 리스트
numbers: List[int] = [1, 2, 3, 4, 5]
names: List[str] = ["Alice", "Bob", "Charlie"]

def get_scores() -> List[float]:
return [85.5, 90.0, 78.5]

# 튜플 (고정 크기)
point: Tuple[int, int] = (10, 20)
rgb: Tuple[int, int, int] = (255, 128, 0)

# 튜플 (가변 크기)
numbers: Tuple[int, ...] = (1, 2, 3, 4, 5)

# 딕셔너리
user: Dict[str, str] = {
"name": "홍길동",
"email": "hong@example.com"
}

scores: Dict[str, int] = {
"math": 85,
"english": 90
}

# 셋
tags: Set[str] = {"python", "programming", "tutorial"}

# 중첩 타입
matrix: List[List[int]] = [
[1, 2, 3],
[4, 5, 6]
]

users: List[Dict[str, Union[str, int]]] = [
{"name": "홍길동", "age": 25},
{"name": "김철수", "age": 30}
]

Python 3.9+ 내장 타입 사용

# Python 3.9+부터는 typing 모듈 불필요
def process_data(numbers: list[int]) -> dict[str, float]:
return {
"sum": sum(numbers),
"average": sum(numbers) / len(numbers)
}

# 튜플
def get_point() -> tuple[int, int]:
return (10, 20)

# 셋
def get_tags() -> set[str]:
return {"python", "coding"}

Optional과 Union

Optional 타입

from typing import Optional

# Optional[T]는 Union[T, None]과 같음
def find_user(user_id: int) -> Optional[str]:
"""사용자 찾기 (없으면 None)"""
users = {1: "홍길동", 2: "김철수"}
return users.get(user_id)

result = find_user(1) # "홍길동"
result = find_user(999) # None

# 기본값이 None인 매개변수
def greet(name: str, greeting: Optional[str] = None) -> str:
if greeting is None:
greeting = "안녕하세요"
return f"{greeting}, {name}님!"

print(greet("홍길동")) # 안녕하세요, 홍길동님!
print(greet("김철수", "반갑습니다")) # 반갑습니다, 김철수님!

# Python 3.10+ (더 간단)
def find_user(user_id: int) -> str | None:
users = {1: "홍길동", 2: "김철수"}
return users.get(user_id)

Union 타입

from typing import Union

# 여러 타입 가능
def process_input(value: Union[int, float, str]) -> str:
if isinstance(value, (int, float)):
return f"숫자: {value}"
return f"문자열: {value}"

print(process_input(42)) # 숫자: 42
print(process_input(3.14)) # 숫자: 3.14
print(process_input("hello")) # 문자열: hello

# Python 3.10+
def process_input(value: int | float | str) -> str:
if isinstance(value, (int, float)):
return f"숫자: {value}"
return f"문자열: {value}"

# 함수 반환값
def divide(a: float, b: float) -> Union[float, str]:
"""나눗셈 (0으로 나누면 에러 메시지)"""
if b == 0:
return "0으로 나눌 수 없습니다"
return a / b

함수 타입 힌트

다양한 함수 시그니처

from typing import List, Tuple, Optional

# 기본 함수
def add(a: int, b: int) -> int:
return a + b

# 여러 반환값
def get_stats(numbers: List[int]) -> Tuple[float, int, int]:
"""평균, 최대, 최소"""
return (
sum(numbers) / len(numbers),
max(numbers),
min(numbers)
)

# 반환값 없음
def print_message(message: str) -> None:
print(message)

# 기본값 매개변수
def create_user(
name: str,
age: int = 0,
email: Optional[str] = None
) -> dict[str, Union[str, int]]:
user = {"name": name, "age": age}
if email:
user["email"] = email
return user

# *args, **kwargs
def sum_all(*numbers: int) -> int:
return sum(numbers)

def print_info(**kwargs: str) -> None:
for key, value in kwargs.items():
print(f"{key}: {value}")

Callable 타입

from typing import Callable

# 함수를 매개변수로
def apply_operation(
x: int,
y: int,
operation: Callable[[int, int], int]
) -> int:
return operation(x, y)

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

def multiply(a: int, b: int) -> int:
return a * b

print(apply_operation(3, 5, add)) # 8
print(apply_operation(3, 5, multiply)) # 15

# 콜백 함수
def process_data(
data: List[int],
callback: Callable[[int], None]
) -> None:
for item in data:
callback(item)

def print_number(n: int) -> None:
print(f"Number: {n}")

process_data([1, 2, 3], print_number)

# 반환값이 함수
def make_multiplier(factor: int) -> Callable[[int], int]:
def multiplier(x: int) -> int:
return x * factor
return multiplier

double = make_multiplier(2)
print(double(5)) # 10

typing 모듈 고급 기능

Any와 NoReturn

from typing import Any, NoReturn

# Any: 모든 타입 허용
def process_value(value: Any) -> str:
return str(value)

# 타입 체크 우회 (권장 안 함)
data: Any = {"key": "value"}
data.some_method() # 에러 체크 안 됨

# NoReturn: 절대 반환하지 않음
def raise_error(message: str) -> NoReturn:
raise ValueError(message)

def infinite_loop() -> NoReturn:
while True:
pass

Literal

from typing import Literal

# 특정 값만 허용
def set_alignment(align: Literal["left", "center", "right"]) -> None:
print(f"정렬: {align}")

set_alignment("left") # OK
set_alignment("center") # OK
# set_alignment("top") # mypy 에러!

# 여러 타입 조합
Mode = Literal["read", "write", "append"]

def open_file(filename: str, mode: Mode) -> None:
print(f"{filename} 파일을 {mode} 모드로 엽니다")

# 숫자 리터럴
def roll_dice() -> Literal[1, 2, 3, 4, 5, 6]:
import random
return random.randint(1, 6) # type: ignore

TypedDict

from typing import TypedDict

# 딕셔너리 구조 정의
class User(TypedDict):
name: str
age: int
email: str

def create_user(name: str, age: int, email: str) -> User:
return {
"name": name,
"age": age,
"email": email
}

user: User = create_user("홍길동", 25, "hong@example.com")

# 선택적 키
class Product(TypedDict, total=False):
name: str # 필수
price: float # 필수
description: str # 선택
stock: int # 선택

product: Product = {
"name": "노트북",
"price": 1500000
} # OK

Generic

from typing import Generic, TypeVar

# 타입 변수
T = TypeVar('T')

class Stack(Generic[T]):
"""제네릭 스택"""

def __init__(self) -> None:
self.items: List[T] = []

def push(self, item: T) -> None:
self.items.append(item)

def pop(self) -> T:
return self.items.pop()

def is_empty(self) -> bool:
return len(self.items) == 0

# 사용
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
print(int_stack.pop()) # 2

str_stack: Stack[str] = Stack()
str_stack.push("hello")
str_stack.push("world")
print(str_stack.pop()) # world

# 제네릭 함수
def first(items: List[T]) -> T:
return items[0]

numbers = [1, 2, 3]
names = ["Alice", "Bob"]

num = first(numbers) # int
name = first(names) # str

클래스 타입 힌트

인스턴스 변수

from typing import List, Optional

class Student:
"""학생 클래스"""

# 클래스 변수
school: str = "파이썬고등학교"

def __init__(self, name: str, age: int) -> None:
# 인스턴스 변수
self.name: str = name
self.age: int = age
self.scores: List[int] = []
self.grade: Optional[str] = None

def add_score(self, score: int) -> None:
self.scores.append(score)

def average(self) -> float:
if not self.scores:
return 0.0
return sum(self.scores) / len(self.scores)

def __str__(self) -> str:
return f"{self.name} ({self.age}세)"

self와 클래스 메서드

from typing import TypeVar

T = TypeVar('T', bound='Builder')

class Builder:
def __init__(self) -> None:
self.result: str = ""

def add(self: T, text: str) -> T:
"""메서드 체이닝을 위해 self 반환"""
self.result += text
return self

@classmethod
def create(cls: type[T]) -> T:
"""팩토리 메서드"""
return cls()

# 사용
builder = Builder.create().add("Hello").add(" ").add("World")
print(builder.result) # Hello World

타입 별칭 (Type Alias)

from typing import List, Dict, Tuple, Union

# 타입 별칭으로 가독성 향상
UserId = int
UserName = str
Score = float

def get_user_name(user_id: UserId) -> UserName:
users = {1: "홍길동", 2: "김철수"}
return users.get(user_id, "Unknown")

# 복잡한 타입 간단히
Vector = List[float]
Matrix = List[Vector]

def dot_product(v1: Vector, v2: Vector) -> float:
return sum(a * b for a, b in zip(v1, v2))

# 딕셔너리 타입
UserData = Dict[str, Union[str, int]]
Users = List[UserData]

def get_users() -> Users:
return [
{"name": "홍길동", "age": 25},
{"name": "김철수", "age": 30}
]

# Python 3.10+ TypeAlias
from typing import TypeAlias

Point: TypeAlias = Tuple[float, float]
Path: TypeAlias = List[Point]

def calculate_distance(path: Path) -> float:
# 경로의 총 거리 계산
total = 0.0
for i in range(len(path) - 1):
x1, y1 = path[i]
x2, y2 = path[i + 1]
total += ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
return total

mypy로 타입 검사

mypy 설치 및 사용

# 설치
pip install mypy

# 파일 검사
mypy script.py

# 프로젝트 전체 검사
mypy .

# 설정 파일 생성
mypy --install-types

mypy 설정

# mypy.ini 또는 setup.cfg
[mypy]
python_version = 3.10
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True

# 특정 모듈 무시
[mypy-numpy.*]
ignore_missing_imports = True

타입 체크 예제

# example.py
def add(a: int, b: int) -> int:
return a + b

# 올바른 사용
result = add(3, 5) # OK

# 잘못된 사용
# result = add("3", "5") # mypy 에러!

# 타입 무시
result = add("3", "5") # type: ignore

# Optional 검사
from typing import Optional

def greet(name: Optional[str]) -> str:
# if name: # mypy 경고: None 체크 필요
# return f"Hello, {name}"
# return "Hello"

# 올바른 방법
if name is not None:
return f"Hello, {name}"
return "Hello"

실전 예제

API 응답 타입

from typing import TypedDict, List, Optional

class UserResponse(TypedDict):
id: int
name: str
email: str
age: int
is_active: bool

class APIResponse(TypedDict):
success: bool
data: Optional[UserResponse]
error: Optional[str]

def fetch_user(user_id: int) -> APIResponse:
"""사용자 정보 조회"""
# API 호출 시뮬레이션
if user_id == 1:
return {
"success": True,
"data": {
"id": 1,
"name": "홍길동",
"email": "hong@example.com",
"age": 25,
"is_active": True
},
"error": None
}
else:
return {
"success": False,
"data": None,
"error": "사용자를 찾을 수 없습니다"
}

# 사용
response = fetch_user(1)
if response["success"] and response["data"]:
user = response["data"]
print(f"{user['name']}: {user['email']}")

제네릭 레포지토리 패턴

from typing import Generic, TypeVar, List, Optional, Protocol

# 엔티티 프로토콜
class Entity(Protocol):
id: int

T = TypeVar('T', bound=Entity)

class Repository(Generic[T]):
"""제네릭 레포지토리"""

def __init__(self) -> None:
self.items: List[T] = []
self.next_id: int = 1

def add(self, item: T) -> T:
"""항목 추가"""
item.id = self.next_id
self.next_id += 1
self.items.append(item)
return item

def get(self, id: int) -> Optional[T]:
"""ID로 조회"""
for item in self.items:
if item.id == id:
return item
return None

def get_all(self) -> List[T]:
"""전체 조회"""
return self.items.copy()

def remove(self, id: int) -> bool:
"""삭제"""
for i, item in enumerate(self.items):
if item.id == id:
del self.items[i]
return True
return False

# 엔티티 정의
class User:
def __init__(self, name: str, email: str) -> None:
self.id: int = 0 # Repository에서 할당
self.name = name
self.email = email

class Product:
def __init__(self, name: str, price: float) -> None:
self.id: int = 0
self.name = name
self.price = price

# 사용
user_repo: Repository[User] = Repository()
product_repo: Repository[Product] = Repository()

user1 = user_repo.add(User("홍길동", "hong@example.com"))
user2 = user_repo.add(User("김철수", "kim@example.com"))

product1 = product_repo.add(Product("노트북", 1500000))
product2 = product_repo.add(Product("마우스", 30000))

print(f"사용자 수: {len(user_repo.get_all())}")
print(f"제품 수: {len(product_repo.get_all())}")

빌더 패턴

from typing import Optional, TypeVar, Generic
from dataclasses import dataclass

@dataclass
class Product:
name: str
price: float
description: Optional[str] = None
stock: int = 0
category: Optional[str] = None

T = TypeVar('T')

class ProductBuilder:
"""제품 빌더"""

def __init__(self) -> None:
self._name: Optional[str] = None
self._price: Optional[float] = None
self._description: Optional[str] = None
self._stock: int = 0
self._category: Optional[str] = None

def with_name(self, name: str) -> 'ProductBuilder':
self._name = name
return self

def with_price(self, price: float) -> 'ProductBuilder':
self._price = price
return self

def with_description(self, description: str) -> 'ProductBuilder':
self._description = description
return self

def with_stock(self, stock: int) -> 'ProductBuilder':
self._stock = stock
return self

def with_category(self, category: str) -> 'ProductBuilder':
self._category = category
return self

def build(self) -> Product:
if self._name is None or self._price is None:
raise ValueError("이름과 가격은 필수입니다")

return Product(
name=self._name,
price=self._price,
description=self._description,
stock=self._stock,
category=self._category
)

# 사용
product = (ProductBuilder()
.with_name("노트북")
.with_price(1500000)
.with_description("고성능 노트북")
.with_stock(10)
.with_category("전자제품")
.build())

print(product)

자주 묻는 질문

Q1. 타입 힌트는 필수인가요?

A: 아니요, 선택사항입니다

# 타입 힌트 없어도 정상 작동
def add(a, b):
return a + b

# 하지만 타입 힌트를 쓰면
# 1. 코드 가독성 향상
# 2. IDE 지원 향상
# 3. 버그 조기 발견
# 4. 문서화 효과

# 추천: 공개 API나 복잡한 코드에는 사용
def calculate_discount(
price: float,
discount_rate: float
) -> float:
return price * (1 - discount_rate)

Q2. 런타임에 타입 체크가 안 되나요?

A: 기본적으로는 안 되지만, 직접 구현 가능

from typing import get_type_hints

def add(a: int, b: int) -> int:
# 런타임 검증
if not isinstance(a, int) or not isinstance(b, int):
raise TypeError("정수만 허용됩니다")
return a + b

# 또는 데코레이터로
def validate_types(func):
def wrapper(*args, **kwargs):
hints = get_type_hints(func)
# 타입 검증 로직
return func(*args, **kwargs)
return wrapper

@validate_types
def multiply(a: int, b: int) -> int:
return a * b

Q3. Any와 타입 힌트를 안 쓰는 것의 차이는?

A: Any는 명시적으로 "모든 타입 허용"을 의미

from typing import Any

# 타입 힌트 없음
def process1(value):
return value

# Any 사용
def process2(value: Any) -> Any:
return value

# 차이점
# 1. Any는 의도적으로 타입을 무시함을 명시
# 2. mypy --disallow-untyped-defs에서
# 타입 힌트 없으면 에러, Any는 통과
# 3. 가독성: Any가 더 명확

Q4. 타입 힌트가 성능에 영향을 주나요?

A: 거의 없습니다

# 타입 힌트는 주석과 비슷
# 런타임에 무시됨 (약간의 메모리만 사용)

import timeit

def add_without_hints(a, b):
return a + b

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

# 성능 차이 거의 없음
print(timeit.timeit(lambda: add_without_hints(1, 2)))
print(timeit.timeit(lambda: add_with_hints(1, 2)))

다음 단계

타입 힌팅을 마스터했습니다!

핵심 정리:
✅ 기본 타입 힌트 (int, str, bool 등)
✅ 컬렉션 타입 (List, Dict, Tuple, Set)
✅ Optional, Union, Literal
✅ Callable, Generic, TypedDict
✅ mypy로 정적 타입 검사
✅ 실전 예제 (API, 레포지토리, 빌더)

다음 단계: 모듈과 패키지에서 코드를 모듈화하고 재사용하는 방법을 배워보세요!