Alert

이 글은 Claude Code의 도움을 λ°›μ•„ μž‘μ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€

TL;DR

  • Python은 동적 νƒ€μž… 언어라 λ³€μˆ˜μ— 아무 νƒ€μž…μ΄λ‚˜ 넣을 수 μžˆμ§€λ§Œ, ν”„λ‘œμ νŠΈκ°€ 컀지면 β€œμ΄ λ³€μˆ˜κ°€ 뭔지” μ•ŒκΈ° μ–΄λ €μ›Œμ§„λ‹€
  • typing λͺ¨λ“ˆμ€ νƒ€μž… 힌트λ₯Ό μ„ μ–Έν•˜λŠ” 도ꡬ λͺ¨μŒμ΄λ©°, λŸ°νƒ€μž„μ—λŠ” 아무 영ν–₯이 μ—†κ³  IDE μžλ™μ™„μ„±κ³Ό νƒ€μž… 체컀(mypy)λ₯Ό μœ„ν•œ 것이닀
  • ν•¨μˆ˜ 인자/λ°˜ν™˜κ°’, λ³€μˆ˜, μ»¬λ ‰μ…˜μ˜ μ›μ†Œ νƒ€μž…κΉŒμ§€ λͺ…μ‹œν•  수 μžˆλ‹€

Sources


1. Why typing

Python은 동적 νƒ€μž… μ–Έμ–΄λ‹€

Python은 λ³€μˆ˜μ— νƒ€μž…μ„ μ„ μ–Έν•˜μ§€ μ•Šμ•„λ„ λœλ‹€. νŽΈν•˜μ§€λ§Œ λ¬Έμ œκ°€ μžˆλ‹€.

def calculate_total(price, quantity, discount):
    return price * quantity * (1 - discount)

이 ν•¨μˆ˜λ§Œ 보면 μ•Œ 수 μ—†λŠ” 것듀:

  • priceκ°€ int인지 float인지 str인지?
  • discountκ°€ 0.1 같은 λΉ„μœ¨μΈμ§€, 10 같은 νΌμ„ΌνŠΈμΈμ§€?
  • λ°˜ν™˜κ°’μ€ λ­”μ§€?
νƒ€μž… 힌트λ₯Ό μΆ”κ°€ν•˜λ©΄
def calculate_total(price: float, quantity: int, discount: float) -> float:
    return price * quantity * (1 - discount)

μ½”λ“œ μžμ²΄κ°€ λ¬Έμ„œκ°€ λœλ‹€. 그리고 IDEκ°€ 이 정보λ₯Ό ν™œμš©ν•œλ‹€:

  • μžλ™μ™„μ„± β€” price.을 치면 float의 λ©”μ„œλ“œκ°€ λœ¬λ‹€
  • 였λ₯˜ 감지 β€” calculate_total("100", 2, 0.1) 같은 μ‹€μˆ˜λ₯Ό μž‘μ•„μ€€λ‹€
  • λ¦¬νŒ©ν† λ§ β€” νƒ€μž…μ„ λ°”κΎΈλ©΄ 영ν–₯λ°›λŠ” 곳을 IDEκ°€ μ•Œλ €μ€€λ‹€

νƒ€μž… νžŒνŠΈλŠ” κ°•μ œκ°€ μ•„λ‹ˆλ‹€

Python은 νƒ€μž… 힌트λ₯Ό λŸ°νƒ€μž„μ— λ¬΄μ‹œν•œλ‹€. νžŒνŠΈμ™€ λ‹€λ₯Έ νƒ€μž…μ„ 넣어도 μ—λŸ¬κ°€ λ‚˜μ§€ μ•ŠλŠ”λ‹€.

def greet(name: str) -> str:
    return f"Hello, {name}"
 
greet(123)  # λŸ°νƒ€μž„ μ—λŸ¬ μ—†μŒ, 정상 싀행됨

νƒ€μž…μ„ κ°•μ œν•˜λ €λ©΄ mypy 같은 νƒ€μž… 체컀λ₯Ό λ³„λ„λ‘œ μ‹€ν–‰ν•˜κ±°λ‚˜, Pydantic처럼 λŸ°νƒ€μž„ 검증을 ν•˜λŠ” 라이브러리λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.


2. Basic type hints

λ³€μˆ˜ νƒ€μž… 힌트
name: str = "alice"
age: int = 30
height: float = 175.5
is_active: bool = True
ν•¨μˆ˜ μΈμžμ™€ λ°˜ν™˜κ°’
def add(a: int, b: int) -> int:
    return a + b
 
def greet(name: str) -> str:
    return f"Hello, {name}"
 
# λ°˜ν™˜κ°’μ΄ μ—†λŠ” ν•¨μˆ˜
def log(message: str) -> None:
    print(message)
기본값이 μžˆλŠ” 인자
def connect(host: str, port: int = 5432, timeout: float = 30.0) -> None:
    ...

μ—¬κΈ°κΉŒμ§€λŠ” typing λͺ¨λ“ˆ 없이 λ‚΄μž₯ νƒ€μž…λ§ŒμœΌλ‘œ μΆ©λΆ„ν•˜λ‹€.


3. Collection types

리슀트, λ”•μ…”λ„ˆλ¦¬ λ“±μ˜ μ•ˆμ— 뭐가 λ“€μ–΄μžˆλŠ”μ§€λ₯Ό ν‘œν˜„ν•˜λ €λ©΄ μ œλ„€λ¦­ ν‘œκΈ°κ°€ ν•„μš”ν•˜λ‹€.

list, dict, set, tuple
# Python 3.9+ : λ‚΄μž₯ νƒ€μž…μ„ κ·ΈλŒ€λ‘œ μ‚¬μš©
names: list[str] = ["alice", "bob"]
scores: dict[str, int] = {"alice": 95, "bob": 87}
tags: set[str] = {"python", "typing"}
 
# tuple은 각 μœ„μΉ˜μ˜ νƒ€μž…μ„ μ§€μ •
point: tuple[float, float] = (1.0, 2.0)
rgb: tuple[int, int, int] = (255, 128, 0)
 
# κ°€λ³€ 길이 tuple
numbers: tuple[int, ...] = (1, 2, 3, 4, 5)
# Python 3.8 μ΄ν•˜: typingμ—μ„œ import
from typing import List, Dict, Set, Tuple
 
names: List[str] = ["alice", "bob"]
scores: Dict[str, int] = {"alice": 95}

Python 3.9 이후

Python 3.9λΆ€ν„° list[str], dict[str, int]처럼 λ‚΄μž₯ νƒ€μž…μ„ 직접 μ œλ„€λ¦­μœΌλ‘œ μ“Έ 수 μžˆλ‹€. typing.List, typing.DictλŠ” 더 이상 ν•„μš” μ—†λ‹€.

쀑첩 μ»¬λ ‰μ…˜
# 2차원 리슀트
matrix: list[list[int]] = [[1, 2], [3, 4]]
 
# λ”•μ…”λ„ˆλ¦¬ μ•ˆμ— 리슀트
user_tags: dict[str, list[str]] = {
    "alice": ["python", "data"],
    "bob": ["java", "spring"],
}
 
# 리슀트 μ•ˆμ— νŠœν”Œ
coordinates: list[tuple[float, float]] = [(1.0, 2.0), (3.0, 4.0)]
ν•¨μˆ˜μ— 적용
def average(scores: list[float]) -> float:
    return sum(scores) / len(scores)
 
def invert(d: dict[str, int]) -> dict[int, str]:
    return {v: k for k, v in d.items()}
 
def unique_words(text: str) -> set[str]:
    return set(text.lower().split())

4. Optional and Union

Union β€” μ—¬λŸ¬ νƒ€μž… 쀑 ν•˜λ‚˜

λ³€μˆ˜κ°€ μ—¬λŸ¬ νƒ€μž…μΌ 수 μžˆμ„ λ•Œ μ‚¬μš©ν•œλ‹€.

# Python 3.10+
def parse_id(value: int | str) -> str:
    return str(value)
 
# Python 3.9 μ΄ν•˜
from typing import Union
 
def parse_id(value: Union[int, str]) -> str:
    return str(value)
# μ‹€μ „: 섀정값이 λ¬Έμžμ—΄ λ˜λŠ” 숫자일 수 μžˆλŠ” 경우
def get_config(key: str) -> str | int | float:
    ...
Optional β€” None이 될 수 μžˆλŠ” νƒ€μž…

Optional[X]λŠ” X | Noneκ³Ό λ™μΌν•˜λ‹€. 값이 없을 수 μžˆλŠ” κ²½μš°μ— μ‚¬μš©ν•œλ‹€.

# Python 3.10+
def find_user(user_id: int) -> str | None:
    ...
 
# Python 3.9 μ΄ν•˜
from typing import Optional
 
def find_user(user_id: int) -> Optional[str]:
    ...
# μ‹€μ „: 검색 κ²°κ³Όκ°€ 없을 수 μžˆλŠ” 경우
def search(query: str) -> list[str] | None:
    results = db.search(query)
    return results if results else None
 
# 기본값이 None인 인자
def connect(host: str, port: int = 5432, password: str | None = None) -> None:
    ...

Optional vs Union

Optional[str]은 μ •ν™•νžˆ str | None이닀. β€œμ„ νƒμ β€μ΄λΌλŠ” 뜻이 μ•„λ‹ˆλΌ None이 될 수 μžˆλ‹€λŠ” λœ»μ΄λ‹€.

# 이 λ‘˜μ€ 동일
def f(x: Optional[str]) -> None: ...
def f(x: str | None) -> None: ...

Python 3.10 이상이면 | 문법이 더 μ§κ΄€μ μ΄λ―€λ‘œ Optional λŒ€μ‹  str | None을 μ“°λŠ” 것이 ꢌμž₯λœλ‹€.


5. Any

νƒ€μž…μ„ μ œν•œν•˜κ³  μ‹Άμ§€ μ•Šμ„ λ•Œ μ‚¬μš©ν•œλ‹€. λͺ¨λ“  νƒ€μž…κ³Ό ν˜Έν™˜λœλ‹€.

from typing import Any
 
def log(value: Any) -> None:
    print(value)
 
data: Any = "hello"
data = 123      # OK
data = [1, 2]   # OK
# dict의 값이 뭐든 될 수 μžˆλŠ” 경우
config: dict[str, Any] = {
    "host": "localhost",
    "port": 5432,
    "debug": True,
    "tags": ["a", "b"],
}

Any vs object

AnyλŠ” νƒ€μž… 체크λ₯Ό μ™„μ „νžˆ λˆλ‹€. objectλŠ” λͺ¨λ“  νƒ€μž…μ˜ λΆ€λͺ¨μ΄μ§€λ§Œ ν•΄λ‹Ή νƒ€μž…μ˜ λ©”μ„œλ“œλ₯Ό μ“Έ 수 μ—†λ‹€.

def f(x: Any) -> None:
    x.foo()      # νƒ€μž… 체컀가 λ¬΄μ‹œ β€” μ—λŸ¬ μ•ˆ 작힘
 
def g(x: object) -> None:
    x.foo()      # νƒ€μž… 체컀 μ—λŸ¬ β€” object에 fooκ°€ μ—†μœΌλ―€λ‘œ

κ°€λŠ₯ν•˜λ©΄ Any λŒ€μ‹  ꡬ체적인 νƒ€μž…μ„ μ“°λŠ” 것이 μ’‹λ‹€.


6. Callable

ν•¨μˆ˜λ₯Ό 인자둜 λ°›κ±°λ‚˜ λ°˜ν™˜ν•  λ•Œ μ‚¬μš©ν•œλ‹€.

from collections.abc import Callable
 
# "str을 λ°›μ•„μ„œ intλ₯Ό λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜"λ₯Ό 인자둜 λ°›λŠ”λ‹€
def apply(func: Callable[[str], int], value: str) -> int:
    return func(value)
 
result = apply(len, "hello")   # 5
result = apply(int, "42")      # 42
# 인자 없이 str을 λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜
def run(factory: Callable[[], str]) -> str:
    return factory()
 
# μ—¬λŸ¬ 인자λ₯Ό λ°›λŠ” ν•¨μˆ˜
def on_event(callback: Callable[[str, int], None]) -> None:
    callback("click", 42)
# 인자 νƒ€μž…μ„ μ œν•œν•˜μ§€ μ•Šμ„ λ•Œ
from typing import Any
 
loose: Callable[..., str]   # 아무 μΈμžλ‚˜ λ°›κ³  str을 λ°˜ν™˜
# μ‹€μ „: λ°μ½”λ ˆμ΄ν„° νƒ€μž… 힌트
from collections.abc import Callable
from functools import wraps
 
def retry(max_attempts: int) -> Callable:
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if i == max_attempts - 1:
                        raise
        return wrapper
    return decorator

7. Literal and Final

Literal β€” νŠΉμ • κ°’λ§Œ ν—ˆμš©
from typing import Literal
 
def open_file(path: str, mode: Literal["r", "w", "a"]) -> None:
    ...
 
open_file("data.txt", "r")      # OK
open_file("data.txt", "x")      # νƒ€μž… 체컀 μ—λŸ¬
# μ‹€μ „: λ°©ν–₯, μƒνƒœ λ“± μ œν•œλœ κ°’
def move(direction: Literal["up", "down", "left", "right"]) -> None:
    ...
 
def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
    ...

Literal vs Enum

λ‘˜ λ‹€ 값을 μ œν•œν•˜λŠ” μš©λ„μ§€λ§Œ μ“°μž„μƒˆκ°€ λ‹€λ₯΄λ‹€.

  • Literal β€” νƒ€μž… 힌트 μ „μš©, λŸ°νƒ€μž„μ— 효과 μ—†μŒ, κ°„λ‹¨ν•œ κ²½μš°μ— 적합
  • Enum β€” λŸ°νƒ€μž„μ—λ„ λ™μž‘, λ©”μ„œλ“œ μΆ”κ°€ κ°€λŠ₯, λ³΅μž‘ν•œ κ²½μš°μ— 적합
Final β€” μž¬ν• λ‹Ή κΈˆμ§€
from typing import Final
 
MAX_RETRIES: Final = 3
MAX_RETRIES = 5   # νƒ€μž… 체컀 μ—λŸ¬
 
API_URL: Final[str] = "https://api.example.com"
# ν΄λž˜μŠ€μ—μ„œ μƒμˆ˜ μ„ μ–Έ
class Config:
    TIMEOUT: Final[int] = 30
    BASE_URL: Final[str] = "https://api.example.com"
 
# 상속 μ‹œ μ˜€λ²„λΌμ΄λ“œ λΆˆκ°€
class CustomConfig(Config):
    TIMEOUT = 60   # νƒ€μž… 체컀 μ—λŸ¬

8. TypeAlias and NewType

TypeAlias β€” κΈ΄ νƒ€μž…μ— 이름 뢙이기

νƒ€μž…μ΄ λ³΅μž‘ν•΄μ§€λ©΄ 별칭을 λ§Œλ“€μ–΄ 가독성을 높일 수 μžˆλ‹€.

# Python 3.12+
type UserId = int
type UserMap = dict[UserId, str]
type Matrix = list[list[float]]
type Handler = Callable[[str, int], None]
# Python 3.10~3.11
from typing import TypeAlias
 
UserId: TypeAlias = int
UserMap: TypeAlias = dict[UserId, str]
Matrix: TypeAlias = list[list[float]]
# 별칭을 μ‚¬μš©ν•˜λ©΄ ν•¨μˆ˜ μ‹œκ·Έλ‹ˆμ²˜κ°€ 깔끔해진닀
def transform(data: Matrix) -> Matrix:
    ...
 
def get_user(users: UserMap, uid: UserId) -> str:
    return users[uid]
NewType β€” 같은 νƒ€μž…μ΄μ§€λ§Œ κ΅¬λΆ„ν•˜κ³  싢을 λ•Œ

NewType은 κΈ°μ‘΄ νƒ€μž…κ³Ό λ™μΌν•˜κ²Œ λ™μž‘ν•˜μ§€λ§Œ, νƒ€μž… 체컀가 λ³„κ°œμ˜ νƒ€μž…μœΌλ‘œ μ·¨κΈ‰ν•œλ‹€.

from typing import NewType
 
UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)
 
def get_user(uid: UserId) -> str:
    ...
 
user_id = UserId(42)
product_id = ProductId(42)
 
get_user(user_id)      # OK
get_user(product_id)   # νƒ€μž… 체컀 μ—λŸ¬ β€” ProductId != UserId
get_user(42)           # νƒ€μž… 체컀 μ—λŸ¬ β€” int != UserId

λŸ°νƒ€μž„μ—λŠ” 아무 νš¨κ³Όκ°€ μ—†λ‹€. UserId(42)λŠ” κ·Έλƒ₯ 42λ₯Ό λ°˜ν™˜ν•œλ‹€. νƒ€μž… μ²΄μ»€λ§Œμ„ μœ„ν•œ μž₯μΉ˜λ‹€.

TypeAlias vs NewType

  • TypeAlias β€” κΈ΄ νƒ€μž…μ˜ μΆ•μ•½. UserId와 intλŠ” μ™„μ „νžˆ λ™μΌν•˜κ²Œ μ·¨κΈ‰
  • NewType β€” κΈ°μ‘΄ νƒ€μž…κ³Ό ꡬ뢄. UserId와 intλŠ” λ³„κ°œμ˜ νƒ€μž…μœΌλ‘œ μ·¨κΈ‰

9. TypedDict

λ”•μ…”λ„ˆλ¦¬μ˜ ν‚€λ³„λ‘œ νƒ€μž…μ„ μ§€μ •ν•  수 μžˆλ‹€. API μ‘λ‹΅μ΄λ‚˜ μ„€μ • 같은 μŠ€ν‚€λ§ˆκ°€ κ³ μ •λœ dict에 μœ μš©ν•˜λ‹€.

from typing import TypedDict
 
class UserDict(TypedDict):
    name: str
    age: int
    email: str
 
user: UserDict = {"name": "alice", "age": 30, "email": "alice@example.com"}
# 일뢀 ν‚€κ°€ 선택적인 경우
class UserDict(TypedDict, total=False):
    name: str       # 선택
    age: int        # 선택
    email: str      # 선택
 
# ν•„μˆ˜ + 선택 μ‘°ν•© (Python 3.11+)
from typing import Required, NotRequired
 
class UserDict(TypedDict):
    name: str                       # ν•„μˆ˜ (κΈ°λ³Έ)
    age: int                        # ν•„μˆ˜ (κΈ°λ³Έ)
    nickname: NotRequired[str]      # 선택
# μ‹€μ „: API 응닡 νƒ€μž… μ •μ˜
class ApiResponse(TypedDict):
    status: int
    data: list[dict[str, str]]
    message: str
 
def handle_response(resp: ApiResponse) -> None:
    if resp["status"] == 200:
        for item in resp["data"]:
            print(item)

TypedDict vs dataclass

λ‘˜ λ‹€ κ΅¬μ‘°ν™”λœ 데이터λ₯Ό ν‘œν˜„ν•˜μ§€λ§Œ λ‹€λ₯΄λ‹€.

  • TypedDict β€” 결과물이 dict κ·ΈλŒ€λ‘œ. JSON 직렬화가 ν•„μš” μ—†κ³ , 기쑴에 dictλ₯Ό μ“°λ˜ 곳에 νƒ€μž…λ§Œ μΆ”κ°€ν•˜κ³  싢을 λ•Œ
  • dataclass β€” 결과물이 클래슀 μΈμŠ€ν„΄μŠ€. λ©”μ„œλ“œ μΆ”κ°€, . μ ‘κ·Ό, 비ꡐ μ—°μ‚° 등이 ν•„μš”ν•  λ•Œ

10. Protocol

β€œμ΄ λ©”μ„œλ“œλ₯Ό κ°€μ§€κ³  있으면 이 νƒ€μž…μœΌλ‘œ μΈμ •ν•œλ‹€β€λŠ” ꡬ쑰적 타이핑(Structural Typing) 을 κ΅¬ν˜„ν•œλ‹€. Java의 μΈν„°νŽ˜μ΄μŠ€μ™€ λΉ„μŠ·ν•˜μ§€λ§Œ, λͺ…μ‹œμ μœΌλ‘œ μƒμ†ν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.

from typing import Protocol
 
class Closable(Protocol):
    def close(self) -> None:
        ...
 
# 이 ν•¨μˆ˜λŠ” close() λ©”μ„œλ“œκ°€ μžˆλŠ” 객체면 뭐든 λ°›λŠ”λ‹€
def cleanup(resource: Closable) -> None:
    resource.close()
# 파일 객체 β€” close()κ°€ μžˆμœΌλ‹ˆ Closable
f = open("test.txt")
cleanup(f)   # OK
 
# μ»€μŠ€ν…€ 클래슀 β€” close()κ°€ μžˆμœΌλ‹ˆ Closable (상속 λΆˆν•„μš”)
class DatabaseConnection:
    def close(self) -> None:
        print("DB μ—°κ²° μ’…λ£Œ")
 
cleanup(DatabaseConnection())   # OK
 
# close()κ°€ μ—†λŠ” 클래슀
class PlainObject:
    pass
 
cleanup(PlainObject())   # νƒ€μž… 체컀 μ—λŸ¬
# μ‹€μ „: μ—¬λŸ¬ λ©”μ„œλ“œλ₯Ό μš”κ΅¬ν•˜λŠ” Protocol
class Readable(Protocol):
    def read(self, size: int = -1) -> str:
        ...
 
    def readline(self) -> str:
        ...
 
def process(source: Readable) -> list[str]:
    lines = []
    while line := source.readline():
        lines.append(line)
    return lines

Protocol vs ABC(Abstract Base Class)

  • ABC β€” 상속이 ν•„μˆ˜. class MyFile(ABC):처럼 λͺ…μ‹œμ μœΌλ‘œ 상속해야 ν•œλ‹€
  • Protocol β€” 상속 λΆˆν•„μš”. λ©”μ„œλ“œ μ‹œκ·Έλ‹ˆμ²˜λ§Œ 맞으면 λœλ‹€ (duck typing의 정적 버전)

11. TypeVar and Generic

ν•¨μˆ˜λ‚˜ ν΄λž˜μŠ€κ°€ μ—¬λŸ¬ νƒ€μž…μ— λŒ€ν•΄ λ™μΌν•˜κ²Œ λ™μž‘ν•  λ•Œ, νƒ€μž… λ³€μˆ˜λ₯Ό μ‚¬μš©ν•΄ β€œμž…λ ₯ νƒ€μž…κ³Ό 좜λ ₯ νƒ€μž…μ˜ 관계”λ₯Ό ν‘œν˜„ν•œλ‹€.

TypeVar
from typing import TypeVar
 
T = TypeVar("T")
 
def first(items: list[T]) -> T:
    return items[0]
 
# νƒ€μž… 체컀가 μΆ”λ‘ :
first([1, 2, 3])        # T = int β†’ λ°˜ν™˜ νƒ€μž… int
first(["a", "b"])       # T = str β†’ λ°˜ν™˜ νƒ€μž… str

Tκ°€ μ—†μœΌλ©΄ μ΄λ ‡κ²Œ μ“Έ μˆ˜λ°–μ— μ—†λ‹€:

# Anyλ₯Ό μ“°λ©΄ λ°˜ν™˜ νƒ€μž… 정보가 사라진닀
def first(items: list[Any]) -> Any:
    return items[0]
 
result = first([1, 2, 3])   # result의 νƒ€μž…: Any (intκ°€ μ•„λ‹˜)
Bound β€” μƒν•œ μ œμ•½
T = TypeVar("T", bound=str)
 
# TλŠ” str λ˜λŠ” str의 μ„œλΈŒν΄λž˜μŠ€λ§Œ κ°€λŠ₯
def to_upper(s: T) -> T:
    return s.upper()
Python 3.12+ 문법

Python 3.12λΆ€ν„°λŠ” TypeVarλ₯Ό λ³„λ„λ‘œ μ„ μ–Έν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.

# Python 3.12+
def first[T](items: list[T]) -> T:
    return items[0]
 
# ν΄λž˜μŠ€μ—λ„ μ‚¬μš© κ°€λŠ₯
class Stack[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()
 
stack = Stack[int]()
stack.push(1)      # OK
stack.push("a")    # νƒ€μž… 체컀 μ—λŸ¬

12. Version changes

λ²„μ „μ£Όμš” λ³€κ²½
3.5typing λͺ¨λ“ˆ λ„μž…
3.9λ‚΄μž₯ μ œλ„€λ¦­ (list[int], dict[str, int]). typing.List λ“± λΆˆν•„μš”
3.10X | Y union 문법. Optional[X] λŒ€μ‹  X | None
3.11TypedDict에 Required, NotRequired μΆ”κ°€
3.12type λ¬Έ, μ œλ„€λ¦­ 문법 def f[T](), class C[T]:

μ–΄λ–€ 버전 문법을 μ“ΈκΉŒ

  • Python 3.10+ β†’ int | str, str | None μ‚¬μš© (κ°€μž₯ 깔끔)
  • Python 3.9 β†’ list[int] μ‚¬μš©, Union은 typing.Union
  • Python 3.8 μ΄ν•˜ β†’ typing.List[int], typing.Optional[str]

from __future__ import annotationsλ₯Ό 파일 맨 μœ„μ— μ“°λ©΄ 3.7~3.9μ—μ„œλ„ list[int], int | str 문법을 μ‚¬μš©ν•  수 μžˆλ‹€. (λ¬Έμžμ—΄λ‘œ 평가가 μ§€μ—°λ˜λŠ” 방식)


13. Practical examples

μ§€κΈˆκΉŒμ§€ 배운 것듀을 μ‘°ν•©ν•œ μ‹€μ „ μ˜ˆμ‹œλ“€μ΄λ‹€.

API client
from typing import Any, TypedDict
 
class ApiResponse(TypedDict):
    status: int
    data: dict[str, Any]
    error: str | None
 
def fetch(url: str, params: dict[str, str] | None = None) -> ApiResponse:
    ...
 
def extract_field(response: ApiResponse, field: str) -> Any:
    return response["data"].get(field)
Data pipeline
from collections.abc import Callable, Iterable
 
type Transform = Callable[[dict[str, Any]], dict[str, Any]]
 
def pipeline(
    data: Iterable[dict[str, Any]],
    transforms: list[Transform],
) -> list[dict[str, Any]]:
    results = list(data)
    for transform in transforms:
        results = [transform(item) for item in results]
    return results
 
# μ‚¬μš©
def add_timestamp(record: dict[str, Any]) -> dict[str, Any]:
    record["processed_at"] = "2026-04-13"
    return record
 
def normalize_keys(record: dict[str, Any]) -> dict[str, Any]:
    return {k.lower(): v for k, v in record.items()}
 
output = pipeline(raw_data, [normalize_keys, add_timestamp])
Config with defaults
from dataclasses import dataclass, field
from typing import Final, Literal
 
LOG_LEVELS = Literal["DEBUG", "INFO", "WARNING", "ERROR"]
 
@dataclass
class AppConfig:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False
    log_level: LOG_LEVELS = "INFO"
    allowed_origins: list[str] = field(default_factory=list)
 
    MAX_CONNECTIONS: Final[int] = 100
Repository pattern
from typing import TypeVar, Protocol
 
class HasId(Protocol):
    id: int
 
T = TypeVar("T", bound=HasId)
 
class Repository:
    def __init__(self) -> None:
        self._store: dict[int, HasId] = {}
 
    def save(self, entity: T) -> T:
        self._store[entity.id] = entity
        return entity
 
    def find(self, entity_id: int) -> HasId | None:
        return self._store.get(entity_id)