한마디 요약
타입 힌트(type hint) 는 “이 변수/파라미터/반환값이 어떤 모양의 데이터를 담을지”를 코드에 박아두는 선언이다. Python 은 이 선언을 실행 중에 강제하지 않는다(런타임 무검증). 대신 IDE, mypy, Pydantic, LangChain 같은 도구가 이걸 읽어서 자동완성, 정적 검사, 스키마 생성, Tool 등록을 수행한다. AI Agent 코드에서는 선택이 아니라 거의 필수.
왜 쓰는가 (한 번만 짚고 간다)
| 관점 | 힌트 없음 | 힌트 있음 |
|---|---|---|
| IDE 자동완성 | 변수. 찍어도 아무것도 안 뜸 | 멤버/메서드 자동 제안 |
| 정적 검사 (mypy, pyright) | 런타임 에러로만 확인 | 커밋 전 타입 오류 검출 |
| 문서 | docstring 없으면 본문 읽어야 함 | 시그니처만 봐도 계약 파악 |
| Pydantic 모델 | 선언 불가 | 필드 타입으로 직결, 자동 검증 |
LangChain @tool | 에러 (타입 힌트 필수) | JSON Schema 자동 추출 |
| FastAPI 라우트 | 불가 | Query/Body/Response 자동 파싱 + Swagger |
| LangGraph State | 타입 추론 실패 | TypedDict 로 노드 함수 안전성 확보 |
핵심 트레이드오프: Python 은 타입 힌트를 런타임에 강제하지 않는다.
x: int = "hello"도 실행은 된다. 힌트는 “도구가 읽는 메타데이터”일 뿐. 실제 검증은 Pydantic / mypy / pyright 같은 외부 도구가 담당.
기본 문법 3가지 위치
# 1. 파라미터 (parameter annotation)
def greet(name: str, age: int = 0) -> None:
# ↑ ↑ ↑
# 파라미터 기본값 있어도 힌트 먼저
print(f"{name}, {age}")
# 2. 반환값 (return annotation, 화살표)
def get_count() -> int:
return 42
# 3. 변수 (variable annotation, PEP 526)
retry_count: int = 0
user_map: dict[str, "User"] = {}포함 관계
함수 타입 힌트 (function type hints)
├─ 파라미터 힌트: def f(x: int, y: list[str]) ← 이 문서 주력
├─ 반환 타입 힌트: -> ReturnType ← [[반환 타입 힌트(화살표➡️)]]
└─ 가변 인자 힌트: def f(*args: int, **kwargs: Any) ← [[함수(별args, 별별kwargs)]]
typing 모듈 주요 타입 지도
Python 3.9 부터는 내장 제네릭(list[int], dict[str, int])이 가능해졌지만, 특수 타입은 여전히 typing 에서 가져와야 한다.
from typing import (
Optional, Union, Any, Literal, Final,
Callable, Awaitable, Coroutine,
TypedDict, NotRequired,
Protocol, TypeVar, Generic, NewType,
Annotated, ClassVar,
)자주 쓰는 것들 요약표
| 타입 | 의미 | AI Agent 사용 예 |
|---|---|---|
Optional[X] | X 또는 None (== Union[X, None]) | 도구 검색 결과 없을 때 Optional[Tool] |
Union[X, Y] / X | Y (3.10+) | 여러 타입 중 하나 | LLM 파싱 결과 dict | str |
Any | 타입 검사 끄기 | 외부 JSON, LLM 원본 응답 |
Literal["a", "b"] | 특정 값만 허용 | 의도 분류 Literal["search","answer"] |
Callable[[Args], Ret] | 함수 타입 | 콜백, 데코레이터 인자 |
Awaitable[X] | await 가능한 것 | async LLM 호출 반환 |
TypedDict | dict 인데 키/값 타입 고정 | LangGraph State 스키마 |
Annotated[T, meta] | 타입 + 메타데이터 | LangGraph Annotated[list, add_messages] |
Protocol | 덕타이핑용 구조적 타입 | ”이 메서드만 있으면 OK” |
TypeVar + Generic | 제네릭 함수/클래스 | 범용 Agent 래퍼 |
실전 예시 1: Langfuse CallbackHandler 시그니처 해석
from typing import Optional, List, Dict, Any
from langfuse.callback import CallbackHandler
def get_callback_handler(
trace_name: str,
tags: Optional[List[str]] = None,
session_id: Optional[str] = None,
user_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> Optional[CallbackHandler]:
...한 줄씩 뜯어보기
tags: Optional[List[str]] = None
| 조각 | 의미 |
|---|---|
List[str] | 문자열들이 담긴 리스트. 예: ["urgent", "test"] |
Optional[List[str]] | 리스트일 수도, None 일 수도 있음 |
= None | 호출 시 안 넘기면 기본값으로 None 들어감 |
metadata: Optional[Dict[str, Any]] = None
Dict[str, Any]: 키는 문자열, 값은 아무 타입이나. LLM trace 같은 자유 형식 메타데이터에 자주 쓴다.
-> Optional[CallbackHandler]
- 반환값은
CallbackHandler객체 또는None. 설정이 없어 핸들러를 만들 수 없으면None을 돌려주는 흔한 “팩토리 실패 패턴”.
실전 예시 2: LangChain @tool 과 타입 힌트
from typing import Literal
from langchain_core.tools import tool
from pydantic import BaseModel, Field
class WeatherQuery(BaseModel):
city: str = Field(description="조회할 도시명")
unit: Literal["celsius", "fahrenheit"] = "celsius"
@tool(args_schema=WeatherQuery)
def get_weather(city: str, unit: Literal["celsius", "fahrenheit"] = "celsius") -> dict:
"""도시의 현재 날씨 정보를 반환합니다."""
return {"city": city, "temp": 22, "unit": unit}
# LangChain 이 내부에서 한 일:
# 1. 파라미터 힌트 → JSON Schema "properties"
# 2. Literal → enum 으로 변환
# 3. 기본값 → required 여부 결정
# 4. docstring → tool description
# 5. LLM 은 이 스키마를 보고 올바른 인자로 호출타입 힌트가 도구 등록의 필수 재료가 된다. 힌트 누락 시 ValueError 발생.
실전 예시 3: LangGraph State 와 TypedDict
from typing import TypedDict, Annotated, NotRequired
from operator import add
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
# Annotated[T, reducer] 가 LangGraph 의 시그니처
messages: Annotated[list[BaseMessage], add] # 노드별 반환을 누적
step: int # 기본은 덮어쓰기
tool_name: NotRequired[str] # 있을 수도, 없을 수도
def planner_node(state: AgentState) -> AgentState:
# state["messages"] 에 자동완성 동작, step 이 int 임을 IDE 가 앎
return {"step": state["step"] + 1, "tool_name": "search"}Annotated 의 두 번째 인자(add)는 LangGraph 런타임이 reducer 로 해석하는 메타데이터. 타입 힌트에 “실행 동작”을 끼워 넣는 예.
실전 예시 4: Pydantic 과의 관계
Pydantic 은 “타입 힌트를 런타임에도 강제하는” 라이브러리. AI Agent 에서 LLM 출력 파싱, API 요청/응답 검증에 필수.
from pydantic import BaseModel, Field
from typing import Optional
class SearchResult(BaseModel):
title: str
url: str
score: float = Field(ge=0.0, le=1.0) # 0 <= x <= 1 검증
snippet: Optional[str] = None
# 타입 힌트 = 검증 규칙
result = SearchResult(title="AI", url="http://...", score=1.5)
# → ValidationError: score must be <= 1.0typing.Optional 은 단순 표시이지만, Pydantic BaseModel 안에 들어가면 실제 검증 로직이 붙는다.
함정: Optional[X] 는 “없어도 됨”이 아니다
def f(x: Optional[int]): # x 는 반드시 전달해야 함. 단, None 도 허용.
...
f() # ❌ TypeError: 인자 누락
f(None) # ✅ 허용
f(3) # ✅ 허용
def g(x: Optional[int] = None): # 이렇게 해야 "생략 가능"
...
g() # ✅Optional[X] = Union[X, None] 일 뿐, “optional parameter(선택 파라미터)” 와는 별개 개념. 선택 파라미터는 = 기본값 이 따로 있어야 한다.
함정: list vs List vs list[int] 버전별 차이
| Python 버전 | 권장 문법 |
|---|---|
| 3.8 이하 | from typing import List 후 List[int] |
| 3.9+ | 내장 list[int] 직접 사용 |
| 3.10+ | X | Y (Union 대체), X | None (Optional 대체) |
실무 프로젝트는 python_requires 가 3.9/3.10 중 어디까지 지원하는지 먼저 본 뒤 문법 결정. Agent 프레임워크(LangChain, LangGraph) 는 보통 3.9+ 를 요구.
직접 확인: 런타임에 힌트 꺼내 보기
from typing import get_type_hints
def handler(msg: str, count: int = 1) -> bool:
return True
print(get_type_hints(handler))
# {'msg': <class 'str'>, 'count': <class 'int'>, 'return': <class 'bool'>}
print(handler.__annotations__)
# 위와 동일. LangChain/Pydantic 이 실제로 이걸 읽어감이 딕셔너리가 Agent 프레임워크들이 Tool 스키마를 만들 때 실제로 참조하는 원천.
관련 문서
- 반환 타입 힌트(화살표➡️)
- 함수(별args, 별별kwargs)
- 함수경로(=none 의 의미) (
= None기본값 패턴) - @데코레이터 (
@tool과 타입 힌트의 만남)
한마디 재요약
타입 힌트는 **Python 에게 하는 선언이 아니라 “도구들에게 하는 약속”**이다. IDE, mypy, Pydantic, LangChain, FastAPI 가 이 약속을 읽고 각자의 방식으로 안전장치를 걸어준다. Agent 코드는 타입 힌트 없이는 거의 돌아가지 않는다.