본문으로 건너뛰기
Advertisement

Ch 2.5 타입 힌트(Type Hints) 입문

타입 힌트는 파이썬 코드에 타입 정보를 명시적으로 표현하는 방법입니다. 실행 시 무시되지만, IDE 지원과 코드 가독성을 크게 향상시킵니다.

1. 타입 힌트란?

Python은 동적 타입 언어이므로 타입을 명시할 필요가 없습니다. 하지만 PEP 484(Python 3.5+)에서 타입 힌트가 도입된 이후, 대규모 프로젝트에서 필수가 되었습니다.

# 타입 힌트 없음 — 무엇을 전달해야 하는지 불명확
def calculate_discount(price, rate):
return price * (1 - rate)

# 타입 힌트 있음 — 명확한 계약
def calculate_discount(price: float, rate: float) -> float:
return price * (1 - rate)

타입 힌트의 핵심 특성:

  • 런타임(실행 시)에 강제되지 않음 — 힌트일 뿐!
  • IDE (VS Code, PyCharm)에서 자동완성과 오류 감지에 활용
  • mypy 같은 정적 분석 도구로 타입 오류 사전 발견
  • 코드 문서화 역할 — 함수 시그니처만 보고도 사용법 파악 가능
# 타입 힌트는 런타임에 강제되지 않음
def add(a: int, b: int) -> int:
return a + b

# 이것도 실행됨! (런타임 오류 없음)
result = add("hello", " world")
print(result) # hello world

# 하지만 mypy 실행 시 오류 감지:
# error: Argument 1 to "add" has incompatible type "str"; expected "int"

2. 변수 어노테이션

# 기본 변수 어노테이션
name: str = "Alice"
age: int = 30
height: float = 165.5
is_active: bool = True

# 초기화 없이 선언만 (타입만 명시)
email: str # 아직 값 없음
phone_number: str | None

# 함수 내 지역변수 어노테이션
def process() -> None:
count: int = 0
items: list[str] = []
mapping: dict[str, int] = {}

3. 함수 시그니처 타입 힌트

# 매개변수 타입 + 반환 타입
def greet(name: str) -> str:
return f"Hello, {name}!"

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

# 반환값 없는 함수는 -> None
def log_message(message: str) -> None:
print(f"[LOG] {message}")

# 기본값이 있는 매개변수
def repeat(text: str, count: int = 3) -> str:
return text * count

# 가변 인수
def sum_all(*args: int) -> int:
return sum(args)

def format_data(**kwargs: str) -> str:
return ", ".join(f"{k}={v}" for k, v in kwargs.items())

print(sum_all(1, 2, 3, 4, 5)) # 15
print(format_data(name="Alice", city="Seoul")) # name=Alice, city=Seoul

4. 기본 타입 힌트

# 빌트인 타입
x: int = 42
y: float = 3.14
z: str = "hello"
flag: bool = True
nothing: None = None

# None을 반환할 수 있는 함수
def find_user(user_id: int) -> str | None:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)

result = find_user(1)
print(result) # Alice
result2 = find_user(99)
print(result2) # None

5. Optional과 Union (Python 3.10+ 문법)

# Python 3.9 이하 — typing 모듈 사용
from typing import Optional, Union

# Optional[str] = str | None
def get_name(user_id: int) -> Optional[str]:
return None if user_id == 0 else "Alice"

# Union[int, str] = int | str
def process(value: Union[int, str]) -> str:
return str(value)

# Python 3.10+ — | 연산자 직접 사용 (권장)
def get_name_v2(user_id: int) -> str | None:
return None if user_id == 0 else "Alice"

def process_v2(value: int | str) -> str:
return str(value)

# 여러 타입 유니온
def parse(data: int | float | str | None) -> str:
if data is None:
return "null"
return str(data)

6. 제네릭 타입 힌트

# Python 3.9+ — 내장 컬렉션에 직접 대괄호 사용 가능
# (이전: from typing import List, Dict, Tuple, Set)

# 리스트
scores: list[int] = [85, 90, 78]
names: list[str] = ["Alice", "Bob"]

# 딕셔너리
config: dict[str, str] = {"host": "localhost", "port": "5432"}
user_scores: dict[str, int] = {"Alice": 95, "Bob": 87}

# 튜플 (고정 길이, 각 위치 타입 지정)
point: tuple[int, int] = (3, 5)
rgb: tuple[int, int, int] = (255, 128, 0)
person: tuple[str, int, float] = ("Alice", 30, 165.5)

# 가변 길이 튜플
numbers: tuple[int, ...] = (1, 2, 3, 4, 5)

# 중첩 컬렉션
matrix: list[list[int]] = [[1, 2, 3], [4, 5, 6]]
graph: dict[str, list[str]] = {"A": ["B", "C"], "B": ["D"]}

# 집합
unique_ids: set[int] = {1, 2, 3}

# Python 3.9 이하에서는 typing 사용
from typing import List, Dict, Tuple, Set
scores_old: List[int] = [85, 90]
config_old: Dict[str, str] = {"key": "value"}

7. mypy로 타입 검사하기

# mypy 설치
pip install mypy

# 타입 검사 실행
mypy script.py

# 엄격 모드 (권장)
mypy --strict script.py

# 설정 파일로 mypy 구성 (pyproject.toml)
# pyproject.toml에 mypy 설정 추가
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
# mypy가 잡아주는 타입 오류 예시

def get_length(text: str) -> int:
return len(text)

# 오류 1: 잘못된 타입 전달
# get_length(42)
# error: Argument 1 to "get_length" has incompatible type "int"; expected "str"

# 오류 2: 반환 타입 불일치
def get_name() -> str:
return None # 오류! str이 아닌 None 반환
# error: Incompatible return value type (got "None", expected "str")

# 올바른 코드
def get_name_safe() -> str | None:
return None # str | None 이므로 OK

# 오류 3: None 체크 없이 사용
def process_user(user_id: int) -> str:
user = get_name_safe()
return user.upper() # 오류! user가 None일 수 있음
# error: Item "None" of "str | None" has no attribute "upper"

# 올바른 처리
def process_user_safe(user_id: int) -> str:
user = get_name_safe()
if user is None:
return "Unknown"
return user.upper() # 이제 user는 str이 보장됨

고수 팁: Any 남용 금지, TYPE_CHECKING 가드로 순환 임포트 해결

Any 남용 금지:

from typing import Any

# 나쁜 예 — Any를 쓰면 타입 검사 무력화
def process(data: Any) -> Any:
return data

# 좋은 예 — 구체적인 타입 사용
from typing import TypeVar

T = TypeVar("T") # 제네릭 타입 변수

def identity(value: T) -> T:
"""입력된 값을 그대로 반환합니다."""
return value

result: int = identity(42) # OK
result2: str = identity("hello") # OK

TYPE_CHECKING 가드로 순환 임포트 해결:

# models.py
from __future__ import annotations # Python 3.10+ 이전 버전 지원
from typing import TYPE_CHECKING

if TYPE_CHECKING:
# 이 임포트는 타입 검사 시에만 실행됨 (런타임에는 실행 안 됨)
from .service import UserService

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

def process(self, service: "UserService") -> None:
# 런타임에 UserService를 임포트하면 순환 임포트 발생
# TYPE_CHECKING 가드 + 문자열 어노테이션으로 해결
service.handle(self)

Callable 타입 힌트:

from collections.abc import Callable

# 인수와 반환 타입을 명시
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)

def add(x: int, y: int) -> int:
return x + y

print(apply(add, 3, 4)) # 7
print(apply(lambda x, y: x * y, 3, 4)) # 12

TypedDict로 딕셔너리 타입 명시:

from typing import TypedDict

class UserConfig(TypedDict):
name: str
age: int
email: str | None

def create_user(config: UserConfig) -> str:
return f"{config['name']} ({config['age']})"

user: UserConfig = {"name": "Alice", "age": 30, "email": None}
print(create_user(user)) # Alice (30)

타입 힌트를 이해했습니다. Ch 2에서 다룬 변수, 기본 타입, 컬렉션, 형변환, 타입 힌트는 파이썬 프로그래밍의 근간입니다. 다음 챕터(Ch 3)에서는 연산자와 표현식을 심층적으로 살펴보겠습니다.

Advertisement