여기서는 "타입 힌트가 런타임을 바꾸는가"보다 어떤 계약을 코드에 드러내려는지를 먼저 봅니다.
Python 타입 힌트는 기본적으로 실행 강제가 아니라 함수 입력·출력과 데이터 모양을 더 선명하게 보여 주는 도구입니다.
핵심 정리
from pathlib import Path
def read_names(path: Path) -> list[str]:
return path.read_text(encoding="utf-8").splitlines()
def find_user(user_id: int) -> dict[str, str] | None:
...문법
기본형은 함수 주석, 변수 주석, 컬렉션 제네릭, union, Callable과 TypeVar, Protocol까지 같이 보면 됩니다.
from typing import Callable, Protocol, TypeVar
def greet(name: str) -> str:
...
items: list[str]
value: int | None
handler: Callable[[int], str]
T = TypeVar("T")
class Closeable(Protocol):
def close(self) -> None: ...함수, 변수, 컬렉션, 선택적 값, 함수 타입, 구조적 계약이 각각 다른 층위의 정보를 준다는 점을 같이 보면 타입 힌트가 훨씬 덜 막연해집니다.
기본 계약
매개변수와 반환값을 먼저 적으면 함수 계약이 선명해진다
가장 기본적인 쓰임은 "이 함수가 무엇을 받아 무엇을 돌려주는가"를 보여 주는 것입니다.
def greet(name: str) -> str:
return f"Hello, {name}"이 정도만 있어도 함수 계약이 훨씬 빨리 읽힙니다.
데이터 모양
컬렉션과 union 표기를 읽을 줄 알면 정보량이 크게 늘어난다
현대 Python에서는 list[str], dict[str, int], set[int]처럼 내장 제네릭 표기가 기본입니다.
def summarize(scores: list[int]) -> dict[str, float]:
return {"avg": sum(scores) / len(scores)}컬렉션 안에 무엇이 들어가는지까지 함께 보여 주므로, 단순 list보다 훨씬 정보량이 큽니다.
names: list[str] = ["Mina", "Jin"]
scores: dict[str, int] = {"mina": 95}
tags: set[str] = {"python", "async"}값이 없을 수 있으면 T | None, 여러 타입이 가능하면 A | B처럼 씁니다.
def find_user(user_id: int) -> dict[str, str] | None:
...
value: int | str중요한 건 타입 힌트를 쓰는 것 자체보다, 호출자가 어떤 분기 처리를 해야 하는지를 드러내는 데 있습니다.
관계 표현
함수 타입, 제네릭 관계, 구조적 계약은 typing 도구를 쓴다
조금 더 전문적인 코드에서는 typing 모듈의 도구가 필요합니다.
from typing import Callable, TypeVar, Protocol
def apply(fn: Callable[[int], str], value: int) -> str:
return fn(value)
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
class Closeable(Protocol):
def close(self) -> None: ...Callable: 함수를 인수로 받을 때TypeVar: 입력과 출력 타입 관계를 연결할 때Protocol: 특정 메서드 시그니처를 만족하는 타입을 표현할 때
런타임 경계
타입 힌트와 런타임 검증은 별개다
타입 힌트를 달았다고 런타임에서 자동 검사가 생기지는 않습니다.
실제 값 검증이 필요하면 isinstance나 직접 검사 코드를 여전히 써야 합니다.
def greet(name: str) -> str:
return f"Hello, {name}"
result = greet(123) # type checker는 경고 가능, 런타임은 자동 차단 안 함주의할 점
타입 힌트를 달았다고 런타임 검사가 자동으로 생기지는 않습니다. 힌트와 실제 값 검증은 별개입니다.
# ❌ 타입 힌트가 자동으로 막아 줄 거라고 기대
def greet(name: str) -> str:
return f"Hello, {name}"
print(greet(123))
# ✅ 타입 힌트는 계약 설명이고, 필요하면 런타임 검증을 직접 추가
def greet(name: str) -> str:
if not isinstance(name, str):
raise TypeError("name must be str")
return f"Hello, {name}"# ❌ 너무 넓은 Any를 쓰면 계약 정보가 사실상 사라진다
from typing import Any
def render(value: Any) -> Any:
return value
# ✅ 정말 여러 타입이면 union이나 Protocol로 더 구체화
def render(value: str | int) -> str:
return str(value)참고 링크
2 sources