18. 타입 힌트

🎯 이 장의 목표
  • 타입 힌트로 함수의 인자·반환 타입을 표시한다.
  • 힌트가 "강제"가 아니라 "표시"임을 이해한다.
  • 리스트·딕셔너리 등 컬렉션 타입을 표기한다.
  • None 가능성을 | None으로 나타내고, 타입 검사 도구를 안다.

먼저: 타입 힌트가 뭔가요?

지금까지 함수를 이렇게 썼습니다.

PYTHON
def greet(name):
    return f"안녕, {name}"

이 함수는 name에 무엇이 와야 할까요? 문자열? 숫자? 코드만 봐서는 확실치 않습니다. 함수를 쓰는 사람도, 나중의 나도 헷갈립니다.

타입 힌트(type hint)는 "이 인자는 문자열, 이 함수는 문자열을 반환한다"를 코드에 명시하는 표기입니다. Python 3.5부터 도입됐고, 지금은 실무의 사실상 표준입니다.

PYTHON
def greet(name: str) -> str:
    return f"안녕, {name}"
  • name: str → 인자 name은 문자열(str)이다
  • -> str → 이 함수는 문자열을 반환한다

읽기만 해도 함수의 입출력이 분명해집니다.

📌 핵심
핵심: 타입 힌트는 인자: 타입-> 반환타입으로 함수의 입출력 타입을 표시한다. 코드를 문서처럼 읽히게 하고, 도구가 버그를 미리 잡게 돕는다.

⚠️ 중요: 힌트는 강제가 아니다

가장 먼저 짚어야 할 점입니다. 타입 힌트는 표시일 뿐, 강제하지 않습니다. 힌트를 어겨도 Python은 그냥 실행합니다.

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)이라 합니다 — 원하는 곳에만, 원하는 만큼만 타입을 붙일 수 있습니다.

📌 핵심
핵심: 타입 힌트는 실행에 영향을 주지 않는다. "이런 타입이 와야 한다"는 약속·문서일 뿐, Python이 막아주지는 않는다. 검사는 별도 도구(아래)가 한다.

그럼 강제하지도 않는데 왜 쓸까요? 세 가지 큰 이점이 있습니다.

  1. 문서화: 주석 없이도 입출력 타입이 드러나 코드가 읽기 쉬워진다.
  2. 에디터 지원: VS Code·PyCharm이 타입을 알면 자동 완성·오타 경고가 정확해진다.
  3. 버그 사전 발견: 타입 검사 도구(mypy·Ruff 등)가 실행 전에 타입 오류를 잡아준다.

기본 타입 표기

자주 쓰는 기본 타입들입니다.

PYTHON
def process(
    name: str,          # 문자열
    age: int,           # 정수
    height: float,      # 실수
    active: bool,       # 불리언
) -> str:
    return f"{name}({age})"
타입의미
str문자열
int정수
float실수
bool불리언
None값 없음 (반환이 없을 때 -> None)

반환값이 없는 함수는 -> None으로 표시합니다.

PYTHON
def log(message: str) -> None:      # 출력만 하고 반환 없음
    print(f"[로그] {message}")

변수에도 힌트

변수에도 타입을 붙일 수 있습니다(필요할 때만).

PYTHON
age: int = 25
name: str = "민지"
scores: list = []

컬렉션 타입

리스트·딕셔너리 같은 컬렉션은 "안에 무엇이 들었는지"까지 표기할 수 있습니다. Python 3.9부터 list·dict를 직접 씁니다.

PYTHON
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, DictList[int]처럼 대문자로 쓴 것을 볼 수 있습니다. Python 3.9+ 에서는 소문자 list[int]가 권장됩니다. 둘은 같은 의미입니다.

None 가능성: | None

값이 있을 수도, 없을 수도(None) 있는 경우가 흔합니다. "찾으면 위치, 없으면 None"처럼요. 이를 타입 | None으로 표기합니다.

PYTHON
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 OptionalOptional[int]로 썼습니다. Optional[int]int | None은 같은 뜻입니다. 새 코드에서는 int | None이 간결해 권장됩니다.

여러 타입이 가능할 때도 |로 잇습니다.

PYTHON
def to_text(value: int | float | str) -> str:    # 셋 중 하나를 받음
    return str(value)

클래스에서 타입 힌트

클래스의 __init__과 속성, 메서드에도 힌트를 붙이면 더 명확해집니다.

PYTHON
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)라는 별도 도구입니다. 코드를 실행하지 않고 정적으로 분석해 타입 오류를 잡아줍니다.

PYTHON
def add(a: int, b: int) -> int:
    return a + b

result = add("hello", "world")   # 타입 검사기가 경고!

이 코드는 Python으로 그냥 실행하면 통과하지만("helloworld"), 타입 검사기를 돌리면 "정수를 받아야 하는데 문자열을 넘겼다"고 미리 알려줍니다.

도구설명
mypy오랜 표준 타입 검사기
Ruff빠른 린터·포매터 (타입 관련 일부 검사 포함)
Pyright / PylanceVS Code에 내장된 빠른 검사기
💡 팁
2026년 현재 Python 도구 생태계는 빠른 Rust 기반 도구로 옮겨가는 중입니다(다음 19장에서 다룹니다). 입문 단계에서는 "VS Code를 쓰면 Pylance가 자동으로 타입을 봐준다" 정도만 알아도 충분합니다. 에디터가 빨간 줄로 타입 오류를 짚어주면 그게 타입 검사가 작동하는 것입니다.

나쁜 예 ❌ vs 좋은 예 ✅

PYTHON
# ❌ 나쁜 예: 타입이 불분명, 주석으로 설명
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으로 실행하면 어떻게 될까요? 타입 검사기를 돌리면?

PYTHON
def shout(text: str) -> str:
    return text.upper()

print(shout(123))

문제 3. 문자열 리스트를 받아 가장 긴 문자열을 반환하되, 리스트가 비었으면 None을 반환하는 함수 longest의 시그니처(인자·반환 타입 힌트)를 작성하세요. 본문도 구현해 보세요.

문제 4. 다음 변수들에 적절한 타입 힌트를 붙이세요.

PYTHON
names = ["민지", "현우"]
ages = {"민지": 25, "현우": 30}
point = (10, 20)

문제 5. int | NoneOptional[int]의 관계를 한 문장으로 설명하세요.

<details>

<summary>✅ 정답·해설 보기</summary>

1.

PYTHON
def multiply(a: int, b: int) -> int:
    return a * b

2. Python으로 실행하면 123.upper()에서 런타임 에러가 납니다(AttributeError: 'int' object has no attribute 'upper'). 타입 검사기는 실행 전에 "str을 받아야 하는데 int를 넘겼다"고 미리 경고합니다 — 타입 힌트의 가치를 보여주는 예입니다.

3.

PYTHON
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.

PYTHON
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. 가상환경·코드 스타일·테스트