νƒ€μž… νžŒνŒ… (Type Hints)

νƒ€μž… νžŒνŒ… (Type Hints)

1. νƒ€μž… νžŒνŒ…μ΄λž€?

νƒ€μž… νžŒνŒ…μ€ 파이썬 3.5+μ—μ„œ λ„μž…λœ κΈ°λŠ₯으둜, λ³€μˆ˜λ‚˜ ν•¨μˆ˜μ˜ νƒ€μž…μ„ λͺ…μ‹œμ μœΌλ‘œ ν‘œμ‹œν•©λ‹ˆλ‹€. νŒŒμ΄μ¬μ€ μ—¬μ „νžˆ 동적 νƒ€μž… μ–Έμ–΄μ΄μ§€λ§Œ, νƒ€μž… 힌트λ₯Ό 톡해 μ½”λ“œμ˜ 가독성과 μ•ˆμ •μ„±μ„ 높일 수 μžˆμŠ΅λ‹ˆλ‹€.

# νƒ€μž… 힌트 없이
def greet(name):
    return f"Hello, {name}"

# νƒ€μž… 힌트 μ‚¬μš©
def greet(name: str) -> str:
    return f"Hello, {name}"

νƒ€μž… νžŒνŒ…μ˜ μž₯점

μž₯점 μ„€λͺ…
가독성 ν•¨μˆ˜μ˜ μž…λ ₯/좜λ ₯ νƒ€μž…μ„ λͺ…ν™•νžˆ μ•Œ 수 있음
IDE 지원 μžλ™μ™„μ„±, νƒ€μž… 였λ₯˜ κ²½κ³ 
λ¬Έμ„œν™” μ½”λ“œ μžμ²΄κ°€ λ¬Έμ„œ μ—­ν• 
버그 예방 정적 λΆ„μ„μœΌλ‘œ λŸ°νƒ€μž„ μ „ 였λ₯˜ 발견

2. κΈ°λ³Έ νƒ€μž… 힌트

κΈ°λ³Έ μžλ£Œν˜•

# λ³€μˆ˜ νƒ€μž… 힌트
name: str = "Python"
age: int = 30
price: float = 19.99
is_active: bool = True
data: bytes = b"hello"

# ν•¨μˆ˜ νƒ€μž… 힌트
def add(a: int, b: int) -> int:
    return a + b

def say_hello(name: str) -> None:
    print(f"Hello, {name}")

μ»¬λ ‰μ…˜ νƒ€μž…

# Python 3.9+ (λ‚΄μž₯ νƒ€μž… 직접 μ‚¬μš©)
numbers: list[int] = [1, 2, 3]
names: set[str] = {"Alice", "Bob"}
scores: dict[str, int] = {"math": 95, "english": 88}
point: tuple[int, int] = (10, 20)
coordinates: tuple[float, ...] = (1.0, 2.0, 3.0)  # κ°€λ³€ 길이

# Python 3.8 μ΄ν•˜ (typing λͺ¨λ“ˆ μ‚¬μš©)
from typing import List, Set, Dict, Tuple

numbers: List[int] = [1, 2, 3]
names: Set[str] = {"Alice", "Bob"}
scores: Dict[str, int] = {"math": 95}
point: Tuple[int, int] = (10, 20)

3. typing λͺ¨λ“ˆ

Optional

값이 None일 수 μžˆμŒμ„ ν‘œμ‹œν•©λ‹ˆλ‹€.

from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    """μ‚¬μš©μžλ₯Ό μ°Ύκ±°λ‚˜ None λ°˜ν™˜"""
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

# Python 3.10+ λŒ€μ•ˆ
def find_user(user_id: int) -> str | None:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

Union

μ—¬λŸ¬ νƒ€μž… 쀑 ν•˜λ‚˜λ₯Ό ν—ˆμš©ν•©λ‹ˆλ‹€.

from typing import Union

def process(value: Union[int, str]) -> str:
    """int λ˜λŠ” str을 λ°›μ•„ λ¬Έμžμ—΄λ‘œ λ³€ν™˜"""
    return str(value)

# Python 3.10+ λŒ€μ•ˆ
def process(value: int | str) -> str:
    return str(value)

Any

λͺ¨λ“  νƒ€μž…μ„ ν—ˆμš©ν•©λ‹ˆλ‹€ (νƒ€μž… 체크 λΉ„ν™œμ„±ν™”).

from typing import Any

def log(message: Any) -> None:
    print(message)

# μ–΄λ–€ 값이든 κ°€λŠ₯
log("hello")
log(123)
log([1, 2, 3])

Callable

ν•¨μˆ˜ νƒ€μž…μ„ μ§€μ •ν•©λ‹ˆλ‹€.

from typing import Callable

# 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

result = apply(add, 3, 4)  # 7

TypeVar

μ œλ„€λ¦­ νƒ€μž… λ³€μˆ˜λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€.

from typing import TypeVar, List

T = TypeVar('T')

def first(items: List[T]) -> T:
    """리슀트의 첫 번째 μš”μ†Œ λ°˜ν™˜"""
    return items[0]

# νƒ€μž…μ΄ μžλ™μœΌλ‘œ 좔둠됨
num = first([1, 2, 3])      # int
name = first(["a", "b"])    # str

4. κ³ κΈ‰ νƒ€μž… νžŒνŒ…

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)

str_stack: Stack[str] = Stack()
str_stack.push("hello")

TypedDict

λ”•μ…”λ„ˆλ¦¬μ˜ 킀와 κ°’ νƒ€μž…μ„ μ •μ˜ν•©λ‹ˆλ‹€.

from typing import TypedDict

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

# μ •ν™•ν•œ 킀와 νƒ€μž…μ΄ ν•„μš”
user: User = {
    "name": "Alice",
    "age": 30,
    "email": "alice@example.com"
}

# 선택적 ν‚€
class UserOptional(TypedDict, total=False):
    name: str
    age: int
    nickname: str  # 선택적

Protocol (ꡬ쑰적 μ„œλΈŒνƒ€μ΄ν•‘)

덕 타이핑을 νƒ€μž… 힌트둜 ν‘œν˜„ν•©λ‹ˆλ‹€.

from typing import Protocol

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

class Circle:
    def draw(self) -> None:
        print("Drawing circle")

class Square:
    def draw(self) -> None:
        print("Drawing square")

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

# Circle, SquareλŠ” Drawable을 μƒμ†ν•˜μ§€ μ•Šμ§€λ§Œ
# draw() λ©”μ„œλ“œκ°€ μžˆμœΌλ―€λ‘œ μ‚¬μš© κ°€λŠ₯
render(Circle())  # OK
render(Square())  # OK

Literal

νŠΉμ • λ¦¬ν„°λŸ΄ κ°’λ§Œ ν—ˆμš©ν•©λ‹ˆλ‹€.

from typing import Literal

def set_mode(mode: Literal["read", "write", "append"]) -> None:
    print(f"Mode: {mode}")

set_mode("read")    # OK
set_mode("write")   # OK
# set_mode("delete")  # νƒ€μž… μ—λŸ¬!

Final

μƒμˆ˜λ₯Ό ν‘œμ‹œν•©λ‹ˆλ‹€ (μž¬ν• λ‹Ή λΆˆκ°€).

from typing import Final

MAX_SIZE: Final = 100
PI: Final[float] = 3.14159

# MAX_SIZE = 200  # νƒ€μž… 체컀가 κ²½κ³ 

5. νƒ€μž… 별칭

λ³΅μž‘ν•œ νƒ€μž…μ— 이름을 λΆ™μž…λ‹ˆλ‹€.

from typing import Dict, List, Tuple

# νƒ€μž… 별칭
UserId = int
UserData = Dict[str, str]
UserList = List[Tuple[UserId, UserData]]

def get_users() -> UserList:
    return [
        (1, {"name": "Alice", "email": "alice@example.com"}),
        (2, {"name": "Bob", "email": "bob@example.com"}),
    ]

# Python 3.10+ TypeAlias
from typing import TypeAlias

Vector: TypeAlias = list[float]
Matrix: TypeAlias = list[Vector]

6. λŸ°νƒ€μž„ vs 정적 뢄석

νƒ€μž… νžŒνŠΈλŠ” λŸ°νƒ€μž„μ— κ°•μ œλ˜μ§€ μ•ŠμŒ

def greet(name: str) -> str:
    return f"Hello, {name}"

# λŸ°νƒ€μž„μ—λŠ” λ¬Έμ œμ—†μ΄ 싀행됨!
result = greet(123)  # "Hello, 123"

정적 νƒ€μž… 체컀 (mypy)

# μ„€μΉ˜
pip install mypy

# νƒ€μž… 체크 μ‹€ν–‰
mypy your_script.py
# example.py
def add(a: int, b: int) -> int:
    return a + b

result = add("hello", "world")  # mypyκ°€ 였λ₯˜ 감지
$ mypy example.py
example.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int"

λŸ°νƒ€μž„ νƒ€μž… 체크 (선택적)

from typing import get_type_hints

def validate_types(func):
    """λŸ°νƒ€μž„ νƒ€μž… 검증 λ°μ½”λ ˆμ΄ν„°"""
    hints = get_type_hints(func)

    def wrapper(*args, **kwargs):
        # 인자 νƒ€μž… 검증
        for (name, value), expected_type in zip(
            kwargs.items(), hints.values()
        ):
            if not isinstance(value, expected_type):
                raise TypeError(f"{name} must be {expected_type}")
        return func(*args, **kwargs)
    return wrapper

7. μ‹€μš©μ  νŒ¨ν„΄

API 응닡 νƒ€μž… μ •μ˜

from typing import TypedDict, List, Optional

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

class User(TypedDict):
    id: int
    name: str
    email: str

class UsersResponse(TypedDict):
    users: List[User]
    total: int
    page: int

def fetch_users(page: int = 1) -> UsersResponse:
    # API 호좜 둜직
    return {
        "users": [{"id": 1, "name": "Alice", "email": "alice@example.com"}],
        "total": 100,
        "page": page
    }

μ„€μ • 클래슀

from typing import Final, ClassVar

class Config:
    # 클래슀 λ³€μˆ˜ (λͺ¨λ“  μΈμŠ€ν„΄μŠ€ 곡유)
    DEBUG: ClassVar[bool] = False
    VERSION: ClassVar[str] = "1.0.0"

    # μƒμˆ˜
    MAX_CONNECTIONS: Final = 100
    TIMEOUT: Final[int] = 30

μ˜€λ²„λ‘œλ“œ

같은 ν•¨μˆ˜μ˜ μ—¬λŸ¬ μ‹œκ·Έλ‹ˆμ²˜λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€.

from typing import overload, Union

@overload
def process(value: int) -> int: ...

@overload
def process(value: str) -> str: ...

def process(value: Union[int, str]) -> Union[int, str]:
    if isinstance(value, int):
        return value * 2
    return value.upper()

# νƒ€μž… 체컀가 μ˜¬λ°”λ₯Έ λ°˜ν™˜ νƒ€μž…μ„ μΆ”λ‘ 
num: int = process(5)       # int
text: str = process("hi")   # str

8. 자주 μ“°λŠ” νƒ€μž… 정리

νƒ€μž… μ„€λͺ… μ˜ˆμ‹œ
int, str, float, bool κΈ°λ³Έ νƒ€μž… x: int = 1
list[T] 리슀트 nums: list[int]
dict[K, V] λ”•μ…”λ„ˆλ¦¬ data: dict[str, int]
set[T] μ§‘ν•© ids: set[int]
tuple[T, ...] νŠœν”Œ point: tuple[int, int]
Optional[T] T λ˜λŠ” None name: Optional[str]
Union[T1, T2] T1 λ˜λŠ” T2 value: Union[int, str]
Callable[[Args], R] ν•¨μˆ˜ νƒ€μž… fn: Callable[[int], str]
Any λͺ¨λ“  νƒ€μž… data: Any
TypeVar μ œλ„€λ¦­ λ³€μˆ˜ T = TypeVar('T')
Protocol ꡬ쑰적 타이핑 class Sized(Protocol)

9. μ—°μŠ΅ 문제

μ—°μŠ΅ 1: ν•¨μˆ˜ νƒ€μž… 힌트

λ‹€μŒ ν•¨μˆ˜μ— μ μ ˆν•œ νƒ€μž… 힌트λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

def calculate_average(numbers):
    if not numbers:
        return None
    return sum(numbers) / len(numbers)

μ—°μŠ΅ 2: μ œλ„€λ¦­ ν•¨μˆ˜

λ¦¬μŠ€νŠΈμ—μ„œ 쑰건을 λ§Œμ‘±ν•˜λŠ” 첫 번째 μš”μ†Œλ₯Ό μ°ΎλŠ” μ œλ„€λ¦­ ν•¨μˆ˜λ₯Ό μž‘μ„±ν•˜μ„Έμš”.

# find_first([1, 2, 3, 4], lambda x: x > 2) -> 3
# find_first(["a", "bb", "ccc"], lambda s: len(s) > 1) -> "bb"

μ—°μŠ΅ 3: TypedDict

μ‚¬μš©μž ν”„λ‘œν•„μ„ λ‚˜νƒ€λ‚΄λŠ” TypedDictλ₯Ό μ •μ˜ν•˜μ„Έμš”. - ν•„μˆ˜: id (int), username (str), email (str) - 선택: bio (str), avatar_url (str)


λ‹€μŒ 단계

02_Decorators.mdμ—μ„œ ν•¨μˆ˜μ™€ 클래슀λ₯Ό ν™•μž₯ν•˜λŠ” λ°μ½”λ ˆμ΄ν„°λ₯Ό λ°°μ›Œλ΄…μ‹œλ‹€!

to navigate between lessons