18. 타입 힌트
- 타입 힌트로 함수의 인자·반환 타입을 표시한다.
- 힌트가 "강제"가 아니라 "표시"임을 이해한다.
- 리스트·딕셔너리 등 컬렉션 타입을 표기한다.
None가능성을| None으로 나타내고, 타입 검사 도구를 안다.
먼저: 타입 힌트가 뭔가요?
지금까지 함수를 이렇게 썼습니다.
def greet(name): return f"안녕, {name}"
이 함수는 name에 무엇이 와야 할까요? 문자열? 숫자? 코드만 봐서는 확실치 않습니다. 함수를 쓰는 사람도, 나중의 나도 헷갈립니다.
타입 힌트(type hint)는 "이 인자는 문자열, 이 함수는 문자열을 반환한다"를 코드에 명시하는 표기입니다. Python 3.5부터 도입됐고, 지금은 실무의 사실상 표준입니다.
def greet(name: str) -> str: return f"안녕, {name}"
name: str→ 인자name은 문자열(str)이다-> str→ 이 함수는 문자열을 반환한다
읽기만 해도 함수의 입출력이 분명해집니다.
인자: 타입과 -> 반환타입으로 함수의 입출력 타입을 표시한다. 코드를 문서처럼 읽히게 하고, 도구가 버그를 미리 잡게 돕는다.⚠️ 중요: 힌트는 강제가 아니다
가장 먼저 짚어야 할 점입니다. 타입 힌트는 표시일 뿐, 강제하지 않습니다. 힌트를 어겨도 Python은 그냥 실행합니다.
def add(a: int, b: int) -> int: return a + b print(add(3, 5)) # 8 print(add("a", "b")) # ab ← 힌트 위반인데도 에러 없이 실행!
add("a", "b")는 "정수를 받는다"는 힌트를 어겼지만, 문자열 +가 동작하므로 그냥 실행됩니다. Python은 실행 시점에 타입 힌트를 검사하지 않습니다.
왜 강제하지 않을까요? Python은 원래 동적 타이핑(dynamic typing) 언어이기 때문입니다. 변수에 무슨 타입이든 자유롭게 담고, 타입은 실행 중에 그때그때 정해집니다(초급편에서 x = 5 했다가 x = "hi"로 바꿔도 됐던 걸 떠올리세요). 이 유연함이 Python의 장점이라, 타입 힌트는 그 유연함을 막지 않으면서 "이런 타입을 의도했다"는 정보만 덧붙이는 방식으로 설계됐습니다. 이를 점진적 타이핑(gradual typing)이라 합니다 — 원하는 곳에만, 원하는 만큼만 타입을 붙일 수 있습니다.
그럼 강제하지도 않는데 왜 쓸까요? 세 가지 큰 이점이 있습니다.
- 문서화: 주석 없이도 입출력 타입이 드러나 코드가 읽기 쉬워진다.
- 에디터 지원: VS Code·PyCharm이 타입을 알면 자동 완성·오타 경고가 정확해진다.
- 버그 사전 발견: 타입 검사 도구(mypy·Ruff 등)가 실행 전에 타입 오류를 잡아준다.
기본 타입 표기
자주 쓰는 기본 타입들입니다.
def process( name: str, # 문자열 age: int, # 정수 height: float, # 실수 active: bool, # 불리언 ) -> str: return f"{name}({age})"
| 타입 | 의미 |
|---|---|
str | 문자열 |
int | 정수 |
float | 실수 |
bool | 불리언 |
None | 값 없음 (반환이 없을 때 -> None) |
반환값이 없는 함수는 -> None으로 표시합니다.
def log(message: str) -> None: # 출력만 하고 반환 없음 print(f"[로그] {message}")
변수에도 힌트
변수에도 타입을 붙일 수 있습니다(필요할 때만).
age: int = 25 name: str = "민지" scores: list = []
컬렉션 타입
리스트·딕셔너리 같은 컬렉션은 "안에 무엇이 들었는지"까지 표기할 수 있습니다. Python 3.9부터 list·dict를 직접 씁니다.
def total(nums: list[int]) -> int: # 정수 리스트를 받아 정수 반환 return sum(nums) def get_scores() -> dict[str, int]: # 문자열→정수 딕셔너리 return {"국어": 90, "수학": 85} names: list[str] = ["민지", "현우"] # 문자열 리스트 coords: tuple[int, int] = (3, 5) # 정수 두 개짜리 튜플
| 표기 | 의미 |
|---|---|
list[int] | 정수들의 리스트 |
list[str] | 문자열들의 리스트 |
dict[str, int] | 키는 문자열, 값은 정수인 딕셔너리 |
tuple[int, int] | 정수 두 개로 된 튜플 |
set[str] | 문자열들의 세트 |
📎list[int]의 대괄호 안int를 "원소의 타입"이라 생각하면 됩니다. "정수가 든 리스트"라는 뜻이죠. 딕셔너리는dict[키타입, 값타입]순서입니다.
from typing import List, Dict 후 List[int]처럼 대문자로 쓴 것을 볼 수 있습니다. Python 3.9+ 에서는 소문자 list[int]가 권장됩니다. 둘은 같은 의미입니다.None 가능성: | None
값이 있을 수도, 없을 수도(None) 있는 경우가 흔합니다. "찾으면 위치, 없으면 None"처럼요. 이를 타입 | None으로 표기합니다.
def find_index(items: list[str], target: str) -> int | None: if target in items: return items.index(target) # 찾으면 정수 위치 return None # 없으면 None print(find_index(["a", "b"], "b")) # 1 print(find_index(["a", "b"], "z")) # None
int | None은 "정수 또는 None"이라는 뜻입니다. 이 |(파이프) 표기는 Python 3.10부터 쓸 수 있습니다.
📎 예전에는from typing import Optional후Optional[int]로 썼습니다.Optional[int]와int | None은 같은 뜻입니다. 새 코드에서는int | None이 간결해 권장됩니다.
여러 타입이 가능할 때도 |로 잇습니다.
def to_text(value: int | float | str) -> str: # 셋 중 하나를 받음 return str(value)
클래스에서 타입 힌트
클래스의 __init__과 속성, 메서드에도 힌트를 붙이면 더 명확해집니다.
class Point: def __init__(self, x: int, y: int) -> None: self.x: int = x self.y: int = y def distance(self) -> float: return (self.x ** 2 + self.y ** 2) ** 0.5 p = Point(3, 4) print(p.distance()) # 5.0
__init__은 보통 -> None으로 표기합니다(객체를 "반환"하지 않으니까요).
타입 검사 도구
힌트는 강제되지 않는다고 했죠. 그럼 누가 검사할까요? 타입 검사기(type checker)라는 별도 도구입니다. 코드를 실행하지 않고 정적으로 분석해 타입 오류를 잡아줍니다.
def add(a: int, b: int) -> int: return a + b result = add("hello", "world") # 타입 검사기가 경고!
이 코드는 Python으로 그냥 실행하면 통과하지만("helloworld"), 타입 검사기를 돌리면 "정수를 받아야 하는데 문자열을 넘겼다"고 미리 알려줍니다.
| 도구 | 설명 |
|---|---|
| mypy | 오랜 표준 타입 검사기 |
| Ruff | 빠른 린터·포매터 (타입 관련 일부 검사 포함) |
| Pyright / Pylance | VS Code에 내장된 빠른 검사기 |
나쁜 예 ❌ vs 좋은 예 ✅
# ❌ 나쁜 예: 타입이 불분명, 주석으로 설명 def calc(items, rate): # items가 뭐지? rate는 비율? 금액? # items: 가격 리스트, rate: 할인율(0~1), 반환: 할인 적용 총액 return sum(items) * (1 - rate) # ✅ 좋은 예: 타입 힌트가 곧 문서 def calc(items: list[float], rate: float) -> float: return sum(items) * (1 - rate)
타입 힌트가 있으면 주석 없이도 입출력이 분명하고, 에디터가 calc([1,2,3], 0.1)을 도와줍니다.
이 장에서 배운 것
- 타입 힌트는
인자: 타입,-> 반환타입으로 입출력 타입을 표시한다. 문서화·에디터 지원·버그 예방에 좋다. - 힌트는 강제가 아니다. 어겨도 Python은 실행한다. 검사는 별도 도구(mypy·Pylance 등)가 한다.
- 컬렉션은
list[int]·dict[str, int]·tuple[int, int]처럼 원소 타입까지 표기한다. - 값이 없을 수 있으면
타입 | None(예전엔Optional[타입]). 여러 타입은|로 잇는다. - 함수 인자·반환 같은 "인터페이스"에 우선 붙이고, 과용하지 않는다.
🧪 실습 문제
문제 1. 두 정수를 받아 곱을 반환하는 함수 multiply에 타입 힌트를 붙여 정의하세요.
문제 2. 다음 코드는 Python으로 실행하면 어떻게 될까요? 타입 검사기를 돌리면?
def shout(text: str) -> str: return text.upper() print(shout(123))
문제 3. 문자열 리스트를 받아 가장 긴 문자열을 반환하되, 리스트가 비었으면 None을 반환하는 함수 longest의 시그니처(인자·반환 타입 힌트)를 작성하세요. 본문도 구현해 보세요.
문제 4. 다음 변수들에 적절한 타입 힌트를 붙이세요.
names = ["민지", "현우"] ages = {"민지": 25, "현우": 30} point = (10, 20)
문제 5. int | None과 Optional[int]의 관계를 한 문장으로 설명하세요.
<details>
<summary>✅ 정답·해설 보기</summary>
1.
def multiply(a: int, b: int) -> int: return a * b
2. Python으로 실행하면 123.upper()에서 런타임 에러가 납니다(AttributeError: 'int' object has no attribute 'upper'). 타입 검사기는 실행 전에 "str을 받아야 하는데 int를 넘겼다"고 미리 경고합니다 — 타입 힌트의 가치를 보여주는 예입니다.
3.
def longest(words: list[str]) -> str | None: if not words: return None return max(words, key=len) print(longest(["hi", "hello", "hey"])) # hello print(longest([])) # None
4.
names: list[str] = ["민지", "현우"] ages: dict[str, int] = {"민지": 25, "현우": 30} point: tuple[int, int] = (10, 20)
5. 둘은 완전히 같은 의미입니다("정수 또는 None"). int | None은 Python 3.10+의 새 문법이고 Optional[int]는 예전 표기로, 새 코드에서는 int | None이 권장됩니다.
</details>
◀️ 이전 장: 17. 데코레이터 | ▶️ 다음 장: 19. 가상환경·코드 스타일·테스트