42. 타입 힌트(type hints) 친절한 입문 (선택)
이 책 전반에 걸쳐, 여러분은 변수에 어떤 데이터 타입이 들어가는지, 또는 함수가 어떤 타입을 받아서 어떤 타입을 반환하는지 명시하지 않고 Python 코드를 작성해 왔습니다. Python은 이런 방식으로도 완벽하게 잘 동작합니다—Python은 동적 타입(dynamically typed) 언어이며, 이는 프로그램이 실행되는 런타임(runtime)에 타입이 결정된다는 뜻입니다. 이런 유연성은 Python의 가장 큰 강점 중 하나로, 코드를 빠르고 표현력 있게 작성할 수 있게 해줍니다.
하지만 프로그램이 더 커지고 복잡해지면, 이런 유연성이 때때로 코드를 이해하고 유지보수하는 일을 어렵게 만들 수 있습니다. 예를 들어 def process_data(items): 같은 함수를 보면, 여러분은 이런 의문이 들 수 있습니다: items에는 어떤 종류의 데이터가 들어 있을까요? 문자열 리스트일까요? 딕셔너리일까요? 아니면 완전히 다른 무언가일까요?
타입 힌트(또는 타입 어노테이션(type annotations)이라고도 함)는 코드에서 기대되는 타입을 문서화하는 방법을 제공합니다. 이는 Python에 추가되는 선택적 기능으로, 코드를 더 명확하게 만들고, 오류를 더 이른 시점에 잡도록 도와주며, 강력한 IDE 기능을 가능하게 합니다—그리고 이 모든 것이 Python이 실제로 코드를 실행하는 방식을 바꾸지 않고도 가능합니다.
이 장에서는 타입 힌트를 부드럽게 소개하면서, 이것이 무엇인지, 왜 존재하는지, 어떻게 효과적으로 사용하는지 보여줍니다. 타입 힌트는 선택 사항이며 Python이 코드를 실행하는 방식에 영향을 주지 않기 때문에, 이 장 전체는 선택(옵션)으로 표시되어 있습니다. 타입 힌트를 전혀 사용하지 않고도 훌륭한 Python 프로그램을 작성할 수 있습니다. 하지만 타입 힌트를 이해하면 현대적인 Python 코드를 읽는 데 도움이 되고, 여러분의 프로젝트에서 언제 도움이 될지 판단하는 데도 도움이 됩니다.
42.1) 왜 타입 힌트가 추가되었을까
Python은 처음부터 동적 타입(dynamically typed) 언어로 만들어졌습니다. 수십 년 동안 Python 프로그래머들은 어떤 타입 정보 없이도 코드를 작성했고, 이는 수많은 프로젝트에서 매우 훌륭하게 작동했습니다. 그렇다면 왜 2015년(Python 3.5와 함께)에 Python에 타입 힌트가 추가되었을까요?
대규모 코드베이스의 도전 과제
Python이 대규모 애플리케이션에서 더 널리 사용되면서, 팀들은 다음과 같은 어려움을 겪게 되었습니다:
# 대규모 코드베이스에서, 이 함수는 무엇을 기대하고 무엇을 반환할까?
def calculate_discount(customer, items, code):
# ... 50 lines of code ...
return result함수 본문 전체나 문서(documentation)를 읽지 않으면, 다음을 알 수 없습니다:
customer는 딕셔너리일까요, 커스텀 객체일까요, 아니면 다른 무언가일까요?items는 리스트(list)일까요, 튜플(tuple)일까요, 아니면 셋(set)일까요?code의 타입은 무엇일까요—문자열일까요, 정수일까요?- 함수는 무엇을 반환할까요—숫자일까요, 딕셔너리일까요, 아니면
None일까요?
작은 프로그램에서는 이런 모호함이 감당 가능한 수준입니다. 함수가 다른 곳에서 어떻게 사용되는지 쉽게 찾아볼 수 있기 때문입니다. 하지만 수십 개의 파일에 걸쳐 수천 개의 함수가 있는 코드베이스에서는, 이런 일이 어려워집니다.
해결책: 선택적 타입 힌트
Python의 창시자들은 타입을 문서화하는 선택적 시스템을 추가하기로 결정했습니다. 핵심 단어는 "선택적"입니다—타입 힌트는 완전히 자발적입니다. 도움이 될 때 사용하고, 그렇지 않을 때는 무시할 수 있으며, 타입 힌트가 있는 코드와 없는 코드를 자유롭게 섞을 수 있습니다.
기본 문법을 보여주는 간단한 예제입니다:
# 타입 힌트 없이
def add(a, b):
return a + b
# 타입 힌트와 함께
def add(a: int, b: int) -> int:
return a + b문법은 간단합니다:
- 파라미터 뒤의 콜론(
:) 은 어떤 타입이어야 하는지 보여줍니다:a: int - 콜론 앞의 화살표(
->) 는 함수가 어떤 타입을 반환하는지 보여줍니다:-> int
이제 앞선 예제에 적용해봅시다:
def calculate_discount(customer: dict, items: list, code: str) -> float:
# ... 50줄의 코드 ...
return result이제 즉시 명확합니다: customer는 딕셔너리, items는 리스트, code는 문자열이고, 함수는 float를 반환합니다.
이 문법이 낯설어도 걱정하지 마세요—42.3-42.6 섹션에서 자세히 살펴볼 것입니다. 지금은 타입 힌트 덕분에 함수가 무엇을 받고 무엇을 반환하는지 한눈에 알 수 있다는 점만 주목하세요.
타입 힌트가 있든 없든 함수는 여전히 정확히 같은 방식으로 작동합니다—Python은 런타임에 이러한 타입을 확인하지 않습니다. (이에 대해서는 42.2에서 자세히 살펴보겠습니다)
점진적이고 실용적인 접근
Python의 타입 힌트 시스템은 다음과 같이 설계되었습니다:
- 선택적(Optional): 타입 힌트를 반드시 사용할 필요가 없습니다
- 점진적(Gradual): 코드의 일부에는 힌트를 추가하고 다른 부분에는 추가하지 않을 수 있습니다
- 비침투적(Non-intrusive): 힌트는 Python이 코드를 실행하는 방식을 바꾸지 않습니다
- 도구 친화적(Tool-friendly): 외부 도구는 힌트를 검사할 수 있지만, Python 자체는 런타임에 이를 무시합니다
이 실용적인 접근은 Python이 유연성을 유지하면서도, 원한다면 이점을 얻을 수 있도록 해줍니다.
42.2) 황금 규칙: 런타임(runtime)에서 강제되지 않는다
타입 힌트에 대해 이해해야 할 가장 중요한 사실은 이것입니다: Python은 런타임에 타입 힌트를 강제하지 않습니다. 타입 힌트는 순전히 정보 제공용입니다. 이 충격적인 현실이 실제로 무엇을 의미하는지 살펴봅시다.
타입 힌트는 잘못된 타입을 막지 않는다
타입 힌트가 있는 이 함수를 보세요:
def greet(name: str) -> str:
return f"Hello, {name}!"
# 42는 문자열이 아닌데도 잘 작동합니다
result = greet(42)
print(result) # 출력: Hello, 42!타입 힌트는 name이 문자열이어야 한다고 명확히 말하지만, Python은 정수 42를 아무 문제 없이 받아들이고 함수를 실행합니다. Python은 타입 힌트를 확인하지 않습니다—그냥 여러분이 제공한 값을 사용할 뿐입니다.
이는 Java나 C++ 같은 언어와 근본적으로 다릅니다. 이런 언어들은 컴파일러가 코드를 실행하기 전에 타입을 확인하고, 타입이 맞지 않으면 실행을 거부합니다. Python의 접근 방식은 더 관대합니다: 올바른 타입을 제공할 것이라고 믿지만, 강제하지는 않습니다.
문제: 동적 타이핑의 위험은 여전하다
진짜 문제는 이것입니다: 타입 힌트를 사용해도, Python의 동적 타이핑 때문에 런타임에만 나타나는 타입 실수를 여전히 할 수 있다는 것입니다:
def calculate_total(prices: list) -> float:
"""가격들의 합계를 계산합니다."""
return sum(prices)
# 이건 잘 작동합니다
print(calculate_total([10.99, 5.50, 3.25])) # 출력: 19.74
# 하지만 이건 런타임에 실패합니다!
print(calculate_total("not a list")) # TypeError: 'str' object is not iterable타입 힌트는 prices가 리스트여야 한다고 명확히 말하지만, Python은 문자열을 전달하는 것을 막지 않습니다. 에러는 코드가 실제로 실행되어 문자열에 sum()을 사용하려고 할 때만 나타납니다.
이건 답답합니다! 이런 문제를 잡기 위해 타입 힌트를 추가했는데, 동적 타이핑의 위험은 여전히 남아있습니다. 타입 에러는 코드 안에 숨어있다가 런타임에, 심지어 사용자가 예상치 못한 행동을 할 때 프로덕션 환경에서 나타날 수 있습니다.
그렇다면 타입 힌트가 런타임 에러를 막지 못한다면, 왜 사용하는 걸까요?
그럼 타입 힌트는 무엇을 위한 것인가?
타입 힌트는 Python의 런타임 동작을 바꾸지는 못하지만, 중요한 목적을 가지고 있습니다—사람과 도구에게 정보를 제공하는 것이지, Python 자체를 위한 것이 아닙니다:
- 문서화: 함수가 어떤 타입을 기대하고 반환하는지 알려줍니다
- IDE 지원: 편집기가 힌트를 사용해 자동완성을 제공하고 경고를 보여줍니다
- 정적 분석: 외부 도구(mypy 같은)가 코드를 실행하기 전에 타입 에러를 확인할 수 있습니다
- 코드 이해: 대규모 코드베이스를 읽고 유지보수하기 쉽게 만듭니다
타입 힌트를 도구가 이해할 수 있는 주석이라고 생각하세요. Python이 실행되는 방식을 바꾸지는 않지만, 더 나은 코드를 작성하는 데 도움을 줍니다.
하지만 이것이 방금 본 런타임 에러를 잡는 데 실제로 어떻게 도움이 될까요?
해결책: 타입 힌트 + IDE 지원
여기서 타입 힌트가 진정으로 빛을 발합니다. Python이 런타임에 타입 힌트를 강제하지 않지만, IDE는 코드를 실행하기도 전에 실수를 잡아낼 수 있습니다:
def add_numbers(a: int, b: int) -> int:
"""두 숫자를 더합니다."""
return a + b
# IDE가 여기서 경고를 보여줍니다 (코드를 실행하기 전에)
result = add_numbers("Hello", "World") # IDE: 경고 - int를 기대했는데 str을 받음코드 편집기가 타입 힌트를 보고 타입 불일치에 대해 타이핑하는 동안 경고할 수 있습니다. 코드를 실행하기 훨씬 전에 말이죠. 이것이 많은 버그를 프로덕션이 아닌 개발 단계에서 잡아냅니다.
현대적인 Python 개발은 일반적으로 이렇게 작동합니다:
- 타입 힌트와 함께 코드를 작성합니다
- IDE가 타입이 맞지 않을 때 경고를 보여줍니다
- 코드를 실행하기 전에 문제를 고칩니다
- 타입 불일치로 인한 런타임 에러가 훨씬 드물어집니다
타입 힌트가 런타임에 에러를 막지는 못하지만, IDE가 이를 사용해서 여러분이 처음부터 버그가 있는 코드를 작성하는 것을 막아줍니다!
두 마리 토끼를 다 잡다
타입 힌트는 Python에게 두 마리 토끼를 다 잡게 해줍니다—대부분의 에러를 조기에 잡으면서도 유연성을 유지합니다:
개발 단계의 안전성: IDE와 타입 체커가 개발 단계에서 대부분의 타입 에러를 잡아내므로, 버그를 일찍 발견할 수 있습니다.
def process(data: list) -> list:
return [x * 2 for x in data]
# 실수로 문자열을 전달하면:
process("hello") # IDE 경고: list를 기대했는데 str을 받음
# 코드를 실행하기 전에 고칩니다!런타임의 유연성: Python은 여전히 타입이 맞지 않는 코드를 실행하므로, 빠른 프로토타이핑이나 의도적으로 여러 타입을 받아들이고 싶을 때 유용합니다.
def add_numbers(a: int, b: int) -> int:
return a + b
# 타입이 맞지 않아도 Python은 실행합니다
print(add_numbers(5.5, 3.2)) # 출력: 8.7 (작동합니다!)
print(add_numbers("Hi", " there")) # 출력: Hi there (이것도 작동합니다!)이 유연성은 여러분이 엄격한 타입 시스템에 갇히지 않는다는 의미입니다. 규칙을 깨야 할 때(테스트, 프로토타이핑, 또는 정당한 사용 사례를 위해), Python은 허용합니다. 하지만 프로덕션 코드를 작성할 때는, IDE가 여러분을 안전하게 지켜줍니다.
황금 규칙을 기억하세요: 타입 힌트는 Python의 런타임 동작을 바꾸지 않습니다—단지 여러분과 도구들에게 문제를 조기에 잡는 데 필요한 정보를 제공할 뿐입니다. 여전히 조심해야 하지만, 이제 여러분의 등을 지켜주는 강력한 동료들이 있습니다.
42.3) 함수 어노테이션: 파라미터와 리턴 값
타입 힌트의 가장 일반적인 사용은 함수 파라미터와 리턴 값에 어노테이션을 다는 것입니다. 이는 독자(그리고 도구들)에게 함수가 어떤 타입을 기대하고 생성하는지 알려줍니다. 가장 단순한 경우부터 시작해서 점진적으로 발전시켜 봅시다.
기본: 파라미터 어노테이션
파라미터에 타입 힌트를 추가하려면, 파라미터 이름 뒤에 콜론을 붙이고 타입을 적습니다:
def greet(name: str):
"""이름으로 사람을 맞이합니다."""
return f"Hello, {name}!"
# 사용법
message = greet("Alice")
print(message) # 출력: Hello, Alice!name: str 문법은 "파라미터 name은 문자열이어야 한다"는 의미입니다. 여러 파라미터에 타입 힌트를 추가할 수 있습니다:
def calculate_area(width: float, height: float):
"""직사각형의 넓이를 계산합니다."""
return width * height
# 사용법
area = calculate_area(5.0, 3.0)
print(area) # 출력: 15.0여기서 width와 height 모두 float로 어노테이션되었습니다. 함수는 전과 똑같이 작동합니다—타입 힌트는 동작을 바꾸지 않습니다—하지만 이제 IDE가 어떤 타입을 기대하는지 알 수 있습니다.
리턴 타입 어노테이션 추가하기
함수가 어떤 타입을 반환하는지 지정하려면, 파라미터 리스트 뒤, 콜론 앞에 -> 타입을 추가합니다:
def get_full_name(first: str, last: str) -> str:
"""이름과 성을 합칩니다."""
return f"{first} {last}"
# 사용법
name = get_full_name("John", "Doe")
print(name) # 출력: John Doe-> str은 "이 함수는 문자열을 반환한다"는 의미입니다. 리턴 타입 어노테이션은 함수 이름만으로는 리턴 타입이 명확하지 않을 때 특히 유용합니다:
def is_adult(age: int) -> bool:
"""누군가가 성인(18세 이상)인지 확인합니다."""
return age >= 18
# 사용법
adult = is_adult(25)
print(adult) # 출력: True구현을 보지 않고도, 이 함수가 불리언 값을 반환한다는 것을 즉시 알 수 있습니다.
같이 사용하기: 완전한 함수
대부분의 함수는 파라미터와 리턴 타입 어노테이션을 모두 가집니다. 완전히 어노테이션된 함수가 어떻게 보이는지 확인해봅시다:
def calculate_discount(price: float, discount_percent: float) -> float:
"""할인된 가격을 계산합니다."""
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# 사용법
original_price = 100.0
discount = 20.0
final_price = calculate_discount(original_price, discount)
print(f"최종 가격: ${final_price:.2f}") # 출력: 최종 가격: $80.00이 함수 시그니처는 알아야 할 모든 것을 알려줍니다:
- 두 개의
float파라미터를 받습니다:price와discount_percent float값을 반환합니다- 구현을 읽지 않아도 이 함수를 어떻게 사용하는지 이해할 수 있습니다
다른 타입을 사용하는 또 다른 예제를 봅시다:
def repeat_message(message: str, times: int) -> str:
"""메시지를 지정된 횟수만큼 반복합니다."""
return message * times
# 사용법
repeated = repeat_message("Hello! ", 3)
print(repeated) # 출력: Hello! Hello! Hello! 타입 힌트는 문자열과 정수를 전달하고, 문자열을 돌려받는다는 것을 명확히 합니다.
기본값과 함께 사용하기
기본값이 있는 파라미터의 경우, 타입 힌트를 파라미터 이름 바로 뒤에 쓰고, 그 다음에 기본값을 지정합니다:
def create_greeting(name: str, formal: bool = False) -> str:
"""인사 메시지를 생성합니다."""
if formal:
return f"Good day, {name}."
return f"Hi, {name}!"
# 사용법
print(create_greeting("Alice")) # 출력: Hi, Alice!
print(create_greeting("Bob", formal=True)) # 출력: Good day, Bob.formal: bool = False 문법은 "formal은 기본값이 False인 불리언이다"라는 의미입니다.
기본값을 가진 여러 파라미터를 모두 어노테이션할 수 있습니다:
def format_price(amount: float, currency: str = "USD", decimals: int = 2) -> str:
"""통화 기호와 함께 가격을 포맷팅합니다."""
if currency == "USD":
symbol = "$"
elif currency == "EUR":
symbol = "€"
else:
symbol = currency
return f"{symbol}{amount:.{decimals}f}"
# 사용법
print(format_price(99.99)) # 출력: $99.99
print(format_price(99.99, "EUR")) # 출력: €99.99
print(format_price(99.995, "USD", 3)) # 출력: $99.995각 파라미터는 타입과 기본값을 명확히 보여주므로, 함수를 이해하고 사용하기 쉽습니다.
특별한 경우: 값을 반환하지 않는 함수
일부 함수는 어떤 동작을 수행(화면에 출력하거나 파일에 쓰기 등)하기만 하고 리턴 값이 없는 경우가 있습니다. 이런 함수들은 리턴값이 없다는 것을 명확히 하기 위해 -> None을 사용합니다:
def print_report(title: str, data: list) -> None:
"""포맷팅된 리포트를 출력합니다."""
print(f"=== {title} ===")
for item in data:
print(f" - {item}")
# return 문이 없으므로 암묵적으로 None을 반환
# 사용법
print_report("Sales Data", [100, 150, 200])출력:
=== Sales Data ===
- 100
- 150
- 200-> None 어노테이션은 이 함수가 의미 있는 값을 반환하지 않는다는 것을 명시적으로 나타냅니다.
왜 -> None을 사용할까요?
- 명확성: 이 함수는 결과값이 아니라 동작을 수행하기 위한 것이라는 의도를 명시합니다
- IDE 지원: 실수로 리턴 값을 사용하려고 하면 IDE가 경고해줍니다
42.4) 간단한 변수 어노테이션
타입 힌트는 주로 함수와 함께 사용되지만, 변수에도 어노테이션을 달 수 있습니다. 어떻게 작동하는지, 그리고 언제 실제로 유용한지 알아봅시다.
기본 변수 어노테이션 문법
변수에 어노테이션을 다는 방법은 함수 파라미터와 동일한 콜론 문법을 사용합니다:
# 변수 어노테이션
name: str = "Alice"
age: int = 30
height: float = 5.7
is_student: bool = True
print(f"{name} is {age} years old") # 출력: Alice is 30 years oldname: str = "Alice" 문법은 "변수 name은 문자열이고 값은 'Alice'이다"라는 의미입니다. 어노테이션은 변수의 작동 방식을 바꾸지 않습니다—순전히 정보 제공용입니다.
그러나 변수 어노테이션은 생략하는 경우가 많음
실무에서 변수 어노테이션은 거의 사용되지 않습니다. 이유는 간단합니다: Python은 값으로부터 타입을 유추할 수 있기 때문에, 어노테이션이 대부분 불필요합니다:
# 이런 어노테이션은 불필요합니다
name: str = "Alice" # 명백히 문자열
count: int = 0 # 명백히 정수
prices: list = [10.99, 5.50] # 명백히 리스트
settings: dict = {} # 명백히 딕셔너리
# 그냥 이렇게 쓰세요
name = "Alice"
count = 0
prices = [10.99, 5.50]
settings = {}name = "Alice"라고 쓰면, 여러분과 IDE 모두 이것이 문자열임을 즉시 알 수 있습니다. 어노테이션은 유용한 정보를 추가하지 않습니다.
실제 Python 코드에서는 변수 어노테이션을 거의 볼 수 없습니다. 이것은 정상이고 당연한 것입니다. 함수 어노테이션이 훨씬 더 중요하고 일반적입니다.
유일하게 유용한 경우: 할당 전에 변수 선언하기
변수 어노테이션이 진정으로 유용한 경우가 하나 있습니다: 값을 할당하기 전에 변수를 선언해야 할 때입니다.
def calculate_statistics(numbers: list) -> dict:
"""숫자 리스트의 기본 통계를 계산합니다."""
# 사용하기 전에 변수 선언
total: float
count: int
average: float
# 이제 값 할당
total = sum(numbers)
count = len(numbers)
average = total / count if count > 0 else 0.0
return {
"total": total,
"count": count,
"average": average
}
# 사용법
result = calculate_statistics([10, 20, 30, 40])
print(f"Average: {result['average']}") # 출력: Average: 25.0어노테이션 없이는 값을 할당하지 않고 변수를 선언할 수 없습니다. 어노테이션을 사용하면 타입을 미리 지정할 수 있어서 코드 구조가 더 명확해질 수 있습니다.
이것이 변수 어노테이션의 주된 실용적 사용 사례입니다.
기억하세요: 변수를 다른 타입으로 재할당할 수 있습니다
타입 어노테이션이 있어도, 변수를 다른 타입으로 재할당할 수 있습니다:
# 문자열로 시작
value: str = "hello"
print(value) # 출력: hello
# 다른 타입으로 재할당 - Python은 허용합니다
value = 42
print(value) # 출력: 42
# 또 다른 타입 변경 - 여전히 허용됩니다
value = [1, 2, 3]
print(value) # 출력: [1, 2, 3]IDE나 정적 타입 체커는 이런 타입 변경에 대해 경고하지만, Python 자체는 막지 않습니다. 타입 힌트는 일관성을 유지하도록 안내하지만 런타임에 강제하지는 않습니다.
42.5) "None" 처리하기: Optional 타입과 | 연산자
Python에서 가장 흔한 패턴 중 하나는 값을 반환할 수도 있고 None을 반환할 수도 있는 함수입니다. 예를 들어, 항목을 검색하면 성공할 수도 있고(항목 반환) 실패할 수도 있습니다(None 반환). 타입 힌트는 이 패턴을 표현하는 명확한 방법을 제공합니다.
문제: None을 반환할 수 있는 함수
사용자를 검색하는 이 함수를 봅시다:
def find_user_by_email(email: str) -> dict:
"""이메일 주소로 사용자를 찾습니다."""
users = [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"}
]
for user in users:
if user["email"] == email:
return user
return None # 타입 불일치! -> dict 힌트와 모순됩니다
# 사용법
user = find_user_by_email("alice@example.com")
if user:
print(f"Found: {user['name']}") # 출력: Found: Alice
else:
print("User not found")타입 힌트 -> dict는 오해의 소지가 있습니다. 함수가 None을 반환할 수 있기 때문입니다. 정적 타입 체커는 None을 반환하는 것이 선언된 반환 타입 dict와 맞지 않는다고 경고할 것입니다.
해결책: Optional 타입을 위한 | 연산자 사용하기
Python 3.10은 타입 힌트를 위한 | 연산자를 도입했는데, 이는 "또는"을 의미합니다. 함수가 한 타입 또는 다른 타입을 반환할 수 있음을 나타내는 데 사용할 수 있습니다:
def find_user_by_email(email: str) -> dict | None:
"""이메일 주소로 사용자를 찾습니다. 찾지 못하면 None을 반환합니다."""
users = [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"}
]
for user in users:
if user["email"] == email:
return user
return None
# 사용법
user = find_user_by_email("alice@example.com")
if user:
print(f"Found: {user['name']}") # 출력: Found: Alice
missing = find_user_by_email("charlie@example.com")
if missing is None:
print("User not found") # 출력: User not found타입 힌트 -> dict | None은 "이 함수는 딕셔너리 또는 None을 반환한다"는 의미입니다. 이것이 함수의 동작을 정확하게 설명합니다.
참고: 오래된 Python 코드(3.10 이전)에서는 dict | None 대신 typing 모듈의 Optional[dict]를 볼 수 있습니다. 둘은 같은 의미이지만, |가 현대적이고 선호되는 문법입니다.
여러 타입과 | 사용하기
|를 사용해서 두 개 이상의 가능한 타입을 나타낼 수 있습니다:
def parse_value(text: str) -> int | float | None:
"""문자열을 숫자로 파싱합니다. 파싱이 실패하면 None을 반환합니다."""
try:
# 먼저 정수로 파싱 시도
if '.' not in text:
return int(text)
# 그렇지 않으면 실수로 파싱
return float(text)
except ValueError:
return None
# 사용법
print(parse_value("42")) # 출력: 42 (int)
print(parse_value("3.14")) # 출력: 3.14 (float)
print(parse_value("invalid")) # 출력: None타입 힌트 -> int | float | None은 함수가 정수, 실수, 또는 None을 반환할 수 있다는 의미입니다.
None 확인하기: 모범 사례
함수가 None을 반환할 수 있을 때는, 결과를 사용하기 전에 항상 None인지 확인하세요. 그렇지 않으면 None을 기대했던 타입인 것처럼 사용하려 할 때 에러가 발생할 수 있습니다:
def get_user_age(user_id: int) -> int | None:
"""사용자의 나이를 가져옵니다. 사용자를 찾지 못하면 None을 반환합니다."""
users = {1: 25, 2: 30, 3: 35}
return users.get(user_id)
# 값을 사용하기 전에 항상 None 확인
age = get_user_age(1)
if age is not None:
print(f"User is {age} years old") # 출력: User is 25 years old
if age >= 18:
print("User is an adult") # 출력: User is an adult
else:
print("User not found")
# 존재하지 않는 사용자의 경우
age = get_user_age(999)
if age is None:
print("User not found") # 출력: User not found핵심은 값을 사용하기 전에 if age is not None: 또는 if age is None:을 사용해서 명시적으로 확인하는 것입니다.
| None과 함께 사용하는 선택적 파라미터
파라미터에도 |를 사용할 수 있으며, 종종 기본값과 함께 사용됩니다:
def format_name(first: str, middle: str | None = None, last: str = "") -> str:
"""전체 이름을 포맷팅합니다. 중간 이름은 선택사항입니다."""
if middle and last:
return f"{first} {middle} {last}"
elif last:
return f"{first} {last}"
return first
# 사용법
print(format_name("John", "Q", "Doe")) # 출력: John Q Doe
print(format_name("Jane", None, "Smith")) # 출력: Jane Smith
print(format_name("Prince")) # 출력: Prince타입 힌트 middle: str | None = None은 middle이 문자열 또는 None일 수 있으며, 기본값이 None임을 나타냅니다. 이것은 선택적 파라미터를 위한 일반적인 패턴입니다.
42.6) 흔히 볼 수 있는 타입 힌트 읽기: list, dict, tuple
다른 사람이 작성한 Python 코드를 읽다 보면 리스트, 딕셔너리, 튜플 같은 컬렉션에 대한 타입 힌트를 접하게 됩니다. 현대 Python은 단순히 무언가가 리스트라는 것뿐만 아니라, 리스트에 어떤 타입의 항목이 들어있는지까지 명시하는 명확한 방법을 제공합니다.
참고: 여기 보여드리는 문법(list[int], dict[str, int] 등)은 Python 3.9+ 에서 작동합니다. 오래된 코드에서는 typing 모듈의 List[int]와 Dict[str, int](대문자)를 볼 수 있는데—같은 방식으로 작동합니다.
기본 컬렉션 타입 힌트
가장 단순한 컬렉션 타입 힌트는 컬렉션 타입만 지정합니다:
def print_items(items: list) -> None:
"""리스트의 모든 항목을 출력합니다."""
for item in items:
print(item)
def get_user_settings() -> dict:
"""사용자 설정을 딕셔너리로 가져옵니다."""
return {"theme": "dark", "notifications": True}
def get_position() -> tuple:
"""x, y 위치를 가져옵니다."""
return (10, 20)이런 힌트는 컬렉션 타입은 알려주지만 내부에 무엇이 있는지는 알려주지 않습니다.
리스트: 항목 타입 지정하기
리스트에 어떤 타입의 항목이 들어있는지 지정하려면, 대괄호를 사용합니다:
def calculate_total(prices: list[float]) -> float:
"""모든 가격의 합계를 계산합니다."""
return sum(prices)
# 사용법
total = calculate_total([10.99, 5.50, 3.25])
print(f"Total: ${total:.2f}") # 출력: Total: $19.74타입 힌트 list[float]는 "실수를 담고 있는 리스트"를 의미합니다. 이것이 단순히 list보다 더 많은 정보를 제공합니다.
문자열을 사용하는 또 다른 예제입니다:
def format_names(names: list[str]) -> str:
"""이름 리스트를 쉼표로 구분된 문자열로 포맷팅합니다."""
return ", ".join(names)
# 사용법
students = ["Alice", "Bob", "Charlie"]
print(format_names(students)) # 출력: Alice, Bob, Charlie타입 힌트 list[str]는 "문자열을 담고 있는 리스트"를 의미합니다.
딕셔너리: 키와 값 타입 지정하기
딕셔너리의 경우, 키 타입과 값 타입을 모두 지정합니다:
def get_student_grades() -> dict[str, int]:
"""학생 이름과 성적을 매핑한 딕셔너리를 가져옵니다."""
return {
"Alice": 95,
"Bob": 87,
"Charlie": 92
}
# 사용법
grades = get_student_grades()
for name, grade in grades.items():
print(f"{name}: {grade}")출력:
Alice: 95
Bob: 87
Charlie: 92타입 힌트 dict[str, int]는 "문자열 키와 정수 값을 가진 딕셔너리"를 의미합니다.
값이 여러 타입일 수 있는 예제입니다:
def get_user_data(user_id: int) -> dict[str, str | int]:
"""사용자 데이터를 가져옵니다. 값은 문자열 또는 정수일 수 있습니다."""
return {
"name": "Alice",
"email": "alice@example.com",
"age": 30,
"id": 12345
}
# 사용법
user = get_user_data(1)
print(f"{user['name']} is {user['age']} years old") # 출력: Alice is 30 years old타입 힌트 dict[str, str | int]는 "문자열 키와 문자열 또는 정수인 값을 가진 딕셔너리"를 의미합니다.
튜플: 고정 길이와 가변 길이
튜플은 종종 고정된 구조를 가지기 때문에 리스트와 다릅니다. 각 위치의 타입을 지정할 수 있습니다:
def get_user_info(user_id: int) -> tuple[str, int, bool]:
"""
사용자 정보를 튜플로 가져옵니다.
반환값: (이름, 나이, 활성 상태)
"""
return ("Alice", 30, True)
# 사용법
name, age, active = get_user_info(1)
print(f"{name}, {age}, active: {active}") # 출력: Alice, 30, active: True타입 힌트 tuple[str, int, bool]는 "정확히 세 개의 요소를 가진 튜플: 문자열, 정수, 불리언 순서"를 의미합니다.
같은 타입의 항목을 가진 가변 길이 튜플의 경우, 생략 부호(...)를 사용합니다:
# 고정 길이 튜플: 정확히 2개의 실수
def get_2d_point() -> tuple[float, float]:
"""2D 좌표 (x, y)를 가져옵니다."""
return (10.5, 20.3)
# 가변 길이 튜플: 임의 개수의 실수
def get_coordinates() -> tuple[float, ...]:
"""좌표를 가져옵니다. 2D, 3D, 또는 어떤 차원이든 가능합니다."""
return (10.5, 20.3, 15.7) # 이 경우 3D
# 사용법
point = get_2d_point()
coords = get_coordinates()
print(f"2D point: {point}") # 출력: 2D point: (10.5, 20.3)
print(f"Coordinates: {coords}") # 출력: Coordinates: (10.5, 20.3, 15.7)타입 힌트 tuple[float, ...]는 "임의 개수의 실수를 담고 있는 튜플"을 의미합니다. ...는 "이 타입의 임의 개수"를 의미합니다.
중첩된 컬렉션
복잡한 데이터 구조를 위해 타입 힌트를 중첩할 수 있습니다. 간단한 예제부터 시작해봅시다:
def get_scores_by_student() -> dict[str, list[int]]:
"""각 학생의 시험 점수를 가져옵니다."""
return {
"Alice": [95, 87, 92],
"Bob": [88, 91, 85],
"Charlie": [90, 88, 94]
}
# 사용법
scores = get_scores_by_student()
for name, tests in scores.items():
average = sum(tests) / len(tests)
print(f"{name}: {average:.1f}")출력:
Alice: 91.3
Bob: 88.0
Charlie: 90.7타입 힌트 dict[str, list[int]]는 "문자열 키와 정수 리스트 값을 가진 딕셔너리"를 의미합니다.
더 복잡한 예제입니다:
def get_student_records() -> list[dict[str, str | int]]:
"""학생 레코드 리스트를 가져옵니다."""
return [
{"name": "Alice", "age": 20, "major": "CS"},
{"name": "Bob", "age": 21, "major": "Math"},
{"name": "Charlie", "age": 19, "major": "Physics"}
]
# 사용법
students = get_student_records()
for student in students:
print(f"{student['name']}, {student['age']}, {student['major']}")출력:
Alice, 20, CS
Bob, 21, Math
Charlie, 19, Physics타입 힌트 list[dict[str, str | int]]는 "딕셔너리의 리스트이며, 각 딕셔너리는 문자열 키와 문자열 또는 정수인 값을 가짐"을 의미합니다.
타입 힌트 읽기: 빠른 참고
코드에서 타입 힌트를 접했을 때, 다음과 같이 읽으면 됩니다:
컬렉션:
list[int]- "정수의 리스트"dict[str, float]- "문자열 키와 실수 값을 가진 딕셔너리"tuple[str, int]- "정확히 두 항목을 가진 튜플: 문자열, 그 다음 정수"tuple[float, ...]- "임의 개수의 실수를 담고 있는 튜플"
Optional과 여러 타입:
int | None- "정수 또는 None"str | int | float- "문자열, 정수, 또는 실수"
중첩:
list[dict[str, int]]- "딕셔너리의 리스트 (각 딕셔너리는 문자열 키와 정수 값을 가짐)"dict[str, list[float]]- "문자열 키와 실수 리스트 값을 가진 딕셔너리"
참고: 오래된 코드(Python < 3.10)에서는 int | str 대신 Union[int, str]를, int | None 대신 Optional[int]를 볼 수 있습니다. 의미는 같습니다.