목차
2 장

변수, 기본 타입과 타입 힌트

파이썬은 동적 언어지만 모던 파이썬은 처음부터 타입을 적습니다. int/str/bool/None과 빌트인 제네릭, int | None 단축 문법, mypy/pyright까지 정리합니다.

1장 시작과 uv 셋업에서 만든 프로젝트 위에서 그대로 이어집니다. 본 챕터의 주제는 변수와 기본 타입, 그리고 타입 힌트입니다.

본 책의 핵심 원칙 중 하나가 “타입힌트 우선"이라는 점은 책 서문에서 적었습니다. 이 원칙이 실제 코드에서 어떻게 드러나는지를 본 챕터에서 처음 만나게 됩니다. 9장에서 다룰 Protocol이나 20장의 ParamSpec 같은 고급 타입 도구도 결국 본 챕터의 기본기 위에 얹는 것이라, 본 챕터를 천천히 읽어두면 뒤가 훨씬 가볍습니다.

파이썬은 동적 타입 언어입니다. 변수에 타입을 적지 않아도 됩니다. 그런데도 모던 파이썬에서는 처음부터 타입을 적는 흐름이 표준이 됐습니다. 왜 그래야 하고, 어떻게 적는지를 정리합니다.

변수 — 선언이 따로 없다 #

다른 언어와 가장 크게 다른 부분입니다. let, var, int x 같은 키워드가 없습니다. 이름에 값을 대입하면 그게 변수입니다.

변수
name = "커티스"
age = 30
height = 175.5
is_admin = True

이름은 snake_case가 관례입니다. JavaScript의 camelCase와 다른 부분이라 처음에 어색합니다. 상수처럼 쓸 값은 대문자 + 언더스코어 (MAX_RETRY = 3).

기본 타입 — 4 + 1 #

가장 자주 쓰는 다섯 개부터 잡으면 됩니다.

기본 타입
i: int = 42
f: float = 3.14
s: str = "hello"
b: bool = True
n: None = None

여기서 : int 부분이 타입 힌트입니다. 변수 이름 뒤에 콜론과 타입을 적습니다. 동작에는 영향이 없지만, 도구(IDE / 타입 체커)가 읽어서 검증과 자동완성을 해 줍니다.

동적 타이핑인데 왜 타입을 적나? #

타입 힌트는 런타임에는 그냥 무시됩니다. 다음 코드는 정상 실행됩니다.

런타임에는 무시됨
x: int = "this is a string"
print(x)   # this is a string

그런데도 다 적는 이유는:

  1. 에디터 자동완성과 정의 점프가 정확해진다 — VS Code, PyCharm 모두 타입을 본다
  2. 타입 체커(mypy / pyright / Pyrefly)가 문제를 컴파일 타임에 잡아줌
  3. 남이 코드를 읽을 때의 문서가 됨 — 함수 시그니처만 보고 무엇을 받고 무엇을 주는지 안다
  4. 리팩터링 안전망 — 함수 시그니처를 바꾸면 어디가 깨지는지 보임

오래된 파이썬 코드는 타입이 없습니다. 새 코드는 가능한 한 타입을 적는 것이 모던 파이썬의 원칙이고, 본 책도 그 원칙을 따릅니다.

타입 힌트 — 변수, 함수, 컬렉션 #

변수 #

변수 어노테이션
count: int = 0
name: str
name = "curtis"     # 선언만 먼저, 값은 나중에

name: str처럼 값 없이 타입만 적는 것도 가능합니다. 클래스 필드 선언에서 자주 쓰입니다(8장 dataclass에서 본격적으로 만납니다).

함수 #

함수 시그니처
def add(a: int, b: int) -> int:
    return a + b

def greet(name: str) -> None:
    print(f"hi, {name}")

매개변수 뒤에 : 타입, 반환은 -> 타입. 반환이 없으면 -> None. 이걸 안 적는 함수는 IDE가 “뭘 반환하는지 모름” 상태가 돼서 그 함수를 쓰는 코드의 자동완성도 같이 흐려집니다.

컬렉션 — 빌트인 제네릭 #

리스트, 딕셔너리 같은 컬렉션은 원소 타입까지 같이 적습니다.

컬렉션 타입
nums: list[int] = [1, 2, 3]
names: list[str] = ["a", "b"]
ages: dict[str, int] = {"curtis": 30, "smith": 25}
unique: set[str] = {"a", "b"}
point: tuple[float, float] = (1.0, 2.0)

list[int]처럼 빌트인 타입에 그대로 []를 쓰는 문법은 Python 3.9부터 지원됩니다. 그 이전에는 from typing import ListList[int]였습니다. 옛 코드에서는 아직 보이지만, 새 코드는 빌트인 쪽으로 통일하면 됩니다.

옛 방식 (3.8 이하) — 새 코드에서는 쓰지 않음
from typing import List, Dict
nums: List[int] = [1, 2, 3]
ages: Dict[str, int] = {"curtis": 30}

부록 A의 옛 파이썬 코드를 modern 스타일로 옮기기에서 이 변환이 도구로 자동화 가능하다는 점을 다시 다룹니다.

None과 옵션 타입 — int | None #

값이 있을 수도, 없을 수도 있는 경우를 표현할 때.

옵션 타입 — 모던 문법
def find_user(id: int) -> str | None:
    if id == 1:
        return "curtis"
    return None

str | None은 Python 3.10부터 지원되는 union 단축 문법입니다. 그 이전에는 Optional[str] 또는 Union[str, None]typing에서 import 해야 했습니다.

옛 방식 — 새 코드에서는 쓰지 않음
from typing import Optional, Union

def find_user(id: int) -> Optional[str]: ...
def parse(value: str) -> Union[int, float, None]: ...

새 코드는 무조건 |입니다. 짧고, 추가 import가 없습니다.

여러 타입 결합
def parse(value: str) -> int | float | None:
    try:
        return int(value)
    except ValueError:
        try:
            return float(value)
        except ValueError:
            return None

숫자 — int와 float #

파이썬의 int임의 정밀도입니다. 64비트 한계가 없습니다.

큰 정수
big = 10 ** 100
print(big)
# 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

float는 IEEE 754 double입니다. 다른 언어와 같은 부동소수점 함정이 그대로 있습니다.

부동소수점
print(0.1 + 0.2)       # 0.30000000000000004
print(0.1 + 0.2 == 0.3)  # False

금융이나 정확한 소수가 필요하면 decimal.Decimal을 씁니다.

정확한 소수
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2"))  # 0.3

숫자 리터럴 작은 팁 #

읽기 좋게 언더스코어를 끼워 넣을 수 있습니다 (3.6+).

가독성
ONE_MILLION = 1_000_000
HEX = 0xFF_FF
BIN = 0b_1010_1010

문자열 — str과 f-string #

작은따옴표 / 큰따옴표 차이는 없습니다. 한 프로젝트에서 일관되게 쓰면 됩니다 (보통 ").

문자열 기본
s1 = "hello"
s2 = 'world'
s3 = """여러 줄
문자열"""

값을 끼워 넣을 때는 f-string을 씁니다.

f-string
name = "curtis"
age = 30

print(f"hi, {name}! you are {age} years old.")
print(f"next year you'll be {age + 1}.")
print(f"{name = }")    # name = 'curtis'  (디버깅에 유용)

f"{변수 = }" 형태는 변수 이름과 값을 같이 출력 합니다. 디버깅용 print에 자주 쓰입니다.

t-string — Python 3.14의 새 문법 #

Python 3.14에서 PEP 750으로 t-string이 추가됐습니다. f-string과 모양이 비슷하지만, 보간이 즉시 일어나지 않고 Template 객체로 남는 것이 차이입니다.

t-string (3.14+)
from string.templatelib import Template

name = "curtis"
tpl: Template = t"hi, {name}!"
# Template 객체. 아직 문자열로 합쳐지지 않은 상태.

# 안전하게 처리하는 함수에 넘김
def render_html(template: Template) -> str: ...
html = render_html(t"<a href='{user_url}'>{user_name}</a>")

언제 쓰나? 사용자 입력을 안전하게 처리해야 하는 경우 — SQL, HTML, shell. f-string은 보간된 결과가 곧장 문자열이라 escape를 잊기 쉬운데, t-string은 라이브러리가 escape 책임을 가져갑니다. 당장 입문 단계에서는 “f-string만 알면 충분”, 다만 라이브러리 코드에서 t-string을 보게 되면 이게 그것이라고 인지하면 됩니다.

bool과 진리값 #

True, False 둘이고, boolint의 서브 타입입니다.

bool은 int
print(True + True)     # 2
print(isinstance(True, int))  # True

흥미롭지만, bool 위치에 int를 섞어 쓰는 건 권장되지 않습니다. 명시적으로 True / False를 쓰세요.

진리값 (truthy / falsy) #

빈 컨테이너와 0은 거짓, 나머지는 참입니다.

falsy 값
if not []:    print("empty list is falsy")
if not "":    print("empty str is falsy")
if not 0:     print("zero is falsy")
if not None:  print("None is falsy")

이걸 이용해 if not items: 같은 표현이 자주 쓰입니다. JavaScript의 if (!arr.length)보다 짧습니다.

타입 변환 — 명시적으로 #

파이썬은 암묵적 형 변환을 거의 하지 않습니다. 정수와 문자열을 더하면 에러가 납니다.

명시적 변환 필요
n = 42
s = "answer: " + str(n)   # str(n) 으로 변환해야 함
# "answer: " + n  → TypeError

age = int(input("나이: "))  # input 은 str 반환 → int 로 변환

자주 쓰는 변환:

자주 쓰는 변환들
str(42)        # '42'
int("42")      # 42
int("42", 16)  # 66 (16진수 해석)
float("3.14")  # 3.14
bool(0)        # False
bool("any")    # True (빈 문자열만 False)
list("abc")    # ['a', 'b', 'c']

타입 별칭 — type 키워드 #

같은 타입 모양이 반복되면 이름을 붙입니다.

타입 별칭 (3.12+)
type UserId = int
type UserName = str
type UserMap = dict[UserId, UserName]

def get_user(id: UserId) -> UserName | None: ...

type 문법은 Python 3.12에서 추가됐습니다. 그 이전에는 다음과 같이 적었습니다.

옛 방식
UserId = int    # 별칭처럼 동작하는 변수
# 또는
from typing import TypeAlias
UserId: TypeAlias = int

새 코드는 type으로 통일하면 됩니다.

타입 체커 — mypy / pyright #

타입 힌트는 런타임에 검증되지 않으므로, 별도 도구가 정적 검증을 합니다. 두 진영이 있습니다.

  • mypy — 가장 오래된 표준. 파이썬 진영 도구. uv add --dev mypy
  • pyright / Pylance — Microsoft 개발. 빠르고 VS Code와 통합 좋음. uv add --dev pyright
  • Pyrefly — Astral 신작 (2026 현재 베타). 더 빠른 차세대 후보

새 프로젝트는 pyright부터 시작하는 게 무난합니다. VS Code에서 Python 확장을 설치하면 Pylance로 자동 동작해서 별도 설정도 거의 필요 없습니다.

pyright 추가
uv add --dev pyright
uv run pyright .

작은 예제로 동작을 보면:

check.py
def add(a: int, b: int) -> int:
    return a + b

result = add("hello", 1)   # ✗ str 은 int 가 아님
$ uv run pyright check.py
check.py:4:18 - error: Argument of type "Literal['hello']" cannot be assigned to parameter "a" of type "int"

런타임 전에 잡힙니다. 처음에는 잔소리처럼 느껴질 수 있는데, 한 달쯤 쓰면 “이걸 어떻게 안 쓰고 살았지” 합니다.

본 챕터에서는 pyright를 한 줄 명령으로만 호출했지만, 30장 타입체커 설정과 CI 통합에서 pyproject.toml 설정·strict 옵션·pre-commit·GitHub Actions까지 정식 운영 셋업을 잡습니다.

연습문제 #

본 챕터의 핵심은 “타입을 적으면 도구가 잡아준다"입니다. 직접 도구가 잡는 모습을 보세요.

  1. 새 프로젝트 typing-play를 만들고 pyright를 dev 의존성으로 추가하세요. play.pydef add(a: int, b: int) -> int: return a + b를 적고, 일부러 add("hello", 1)를 호출한 뒤 uv run pyright play.py가 에러를 잡는 것을 확인하세요.
  2. play.pydef first(items: list[int]) -> int | None: return items[0] if items else None를 정의하고, first([1, 2, 3])first([]) 둘 다 동작하는지 확인하세요. 반환 타입이 int | None 이라서 호출 쪽에서 result + 1 같은 코드를 적으면 pyright가 None 가능성을 지적합니다. 그 에러도 직접 보세요.
  3. type 키워드로 type UserId = inttype UserMap = dict[UserId, str]을 정의하고, UserMap을 인자로 받는 함수를 하나 작성하세요. pyright가 UserIdint를 서로 호환되는 것으로 인식하는지 (3.12+ 의 type 문법은 단순 별칭임) 확인합니다.

한 줄 요약: 모던 파이썬은 변수 / 함수 / 컬렉션 / 옵션에 모두 타입을 적는다. 빌트인 제네릭 list[int], 옵션은 T | None, 별칭은 type Name = ..., 도구는 pyright. 런타임 동작에는 영향이 없지만 IDE · 타입 체커 · 리팩터링이 전부 달라진다.

다음 챕터 #

다음 3장 제어 흐름 — if, while, for, match-case에서는 흐름 제어 — if, while, for, 그리고 3.10에서 들어온 **match-case**를 다룹니다. 다른 언어의 switch와 동작 방식이 어떻게 다른지가 핵심입니다. 본 챕터에서 잡은 타입힌트는 3장의 match-case 패턴 분기에서 한 번 더 활용됩니다.

X