Alert
μ΄ κΈμ Claude Codeμ λμμ λ°μ μμ±λμμ΅λλ€
TL;DR
- Pythonμ λμ νμ μΈμ΄λΌ λ³μμ μ무 νμ μ΄λ λ£μ μ μμ§λ§, νλ‘μ νΈκ° 컀μ§λ©΄ βμ΄ λ³μκ° λμ§β μκΈ° μ΄λ €μμ§λ€
typingλͺ¨λμ νμ ννΈλ₯Ό μ μΈνλ λꡬ λͺ¨μμ΄λ©°, λ°νμμλ μ무 μν₯μ΄ μκ³ IDE μλμμ±κ³Ό νμ 체컀(mypy)λ₯Ό μν κ²μ΄λ€- ν¨μ μΈμ/λ°νκ°, λ³μ, 컬λ μ μ μμ νμ κΉμ§ λͺ μν μ μλ€
Sources
1. Why typing
Pythonμ λμ νμ μΈμ΄λ€
Pythonμ λ³μμ νμ μ μ μΈνμ§ μμλ λλ€. νΈνμ§λ§ λ¬Έμ κ° μλ€.
def calculate_total(price, quantity, discount):
return price * quantity * (1 - discount)μ΄ ν¨μλ§ λ³΄λ©΄ μ μ μλ κ²λ€:
priceκ°intμΈμ§floatμΈμ§strμΈμ§?discountκ° 0.1 κ°μ λΉμ¨μΈμ§, 10 κ°μ νΌμΌνΈμΈμ§?- λ°νκ°μ λμ§?
νμ ννΈλ₯Ό μΆκ°νλ©΄
def calculate_total(price: float, quantity: int, discount: float) -> float:
return price * quantity * (1 - discount)μ½λ μμ²΄κ° λ¬Έμκ° λλ€. κ·Έλ¦¬κ³ IDEκ° μ΄ μ 보λ₯Ό νμ©νλ€:
- μλμμ± β
price.μ μΉλ©΄ floatμ λ©μλκ° λ¬λ€ - μ€λ₯ κ°μ§ β
calculate_total("100", 2, 0.1)κ°μ μ€μλ₯Ό μ‘μμ€λ€ - 리ν©ν λ§ β νμ μ λ°κΎΈλ©΄ μν₯λ°λ κ³³μ IDEκ° μλ €μ€λ€
νμ ννΈλ κ°μ κ° μλλ€
Pythonμ νμ ννΈλ₯Ό λ°νμμ 무μνλ€. ννΈμ λ€λ₯Έ νμ μ λ£μ΄λ μλ¬κ° λμ§ μλλ€.
def greet(name: str) -> str: return f"Hello, {name}" greet(123) # λ°νμ μλ¬ μμ, μ μ μ€νλ¨νμ μ κ°μ νλ €λ©΄ mypy κ°μ νμ 체컀λ₯Ό λ³λλ‘ μ€ννκ±°λ, Pydanticμ²λΌ λ°νμ κ²μ¦μ νλ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ¬μ©ν΄μΌ νλ€.
2. Basic type hints
λ³μ νμ ννΈ
name: str = "alice"
age: int = 30
height: float = 175.5
is_active: bool = Trueν¨μ μΈμμ λ°νκ°
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}"
# λ°νκ°μ΄ μλ ν¨μ
def log(message: str) -> None:
print(message)κΈ°λ³Έκ°μ΄ μλ μΈμ
def connect(host: str, port: int = 5432, timeout: float = 30.0) -> None:
...μ¬κΈ°κΉμ§λ typing λͺ¨λ μμ΄ λ΄μ₯ νμ
λ§μΌλ‘ μΆ©λΆνλ€.
3. Collection types
리μ€νΈ, λμ λ리 λ±μ μμ λκ° λ€μ΄μλμ§λ₯Ό νννλ €λ©΄ μ λ€λ¦ νκΈ°κ° νμνλ€.
list, dict, set, tuple
# Python 3.9+ : λ΄μ₯ νμ
μ κ·Έλλ‘ μ¬μ©
names: list[str] = ["alice", "bob"]
scores: dict[str, int] = {"alice": 95, "bob": 87}
tags: set[str] = {"python", "typing"}
# tupleμ κ° μμΉμ νμ
μ μ§μ
point: tuple[float, float] = (1.0, 2.0)
rgb: tuple[int, int, int] = (255, 128, 0)
# κ°λ³ κΈΈμ΄ tuple
numbers: tuple[int, ...] = (1, 2, 3, 4, 5)# Python 3.8 μ΄ν: typingμμ import
from typing import List, Dict, Set, Tuple
names: List[str] = ["alice", "bob"]
scores: Dict[str, int] = {"alice": 95}Python 3.9 μ΄ν
Python 3.9λΆν°
list[str],dict[str, int]μ²λΌ λ΄μ₯ νμ μ μ§μ μ λ€λ¦μΌλ‘ μΈ μ μλ€.typing.List,typing.Dictλ λ μ΄μ νμ μλ€.
μ€μ²© 컬λ μ
# 2μ°¨μ 리μ€νΈ
matrix: list[list[int]] = [[1, 2], [3, 4]]
# λμ
λ리 μμ 리μ€νΈ
user_tags: dict[str, list[str]] = {
"alice": ["python", "data"],
"bob": ["java", "spring"],
}
# 리μ€νΈ μμ νν
coordinates: list[tuple[float, float]] = [(1.0, 2.0), (3.0, 4.0)]ν¨μμ μ μ©
def average(scores: list[float]) -> float:
return sum(scores) / len(scores)
def invert(d: dict[str, int]) -> dict[int, str]:
return {v: k for k, v in d.items()}
def unique_words(text: str) -> set[str]:
return set(text.lower().split())4. Optional and Union
Union β μ¬λ¬ νμ μ€ νλ
λ³μκ° μ¬λ¬ νμ μΌ μ μμ λ μ¬μ©νλ€.
# Python 3.10+
def parse_id(value: int | str) -> str:
return str(value)
# Python 3.9 μ΄ν
from typing import Union
def parse_id(value: Union[int, str]) -> str:
return str(value)# μ€μ : μ€μ κ°μ΄ λ¬Έμμ΄ λλ μ«μμΌ μ μλ κ²½μ°
def get_config(key: str) -> str | int | float:
...Optional β Noneμ΄ λ μ μλ νμ
Optional[X]λ X | Noneκ³Ό λμΌνλ€. κ°μ΄ μμ μ μλ κ²½μ°μ μ¬μ©νλ€.
# Python 3.10+
def find_user(user_id: int) -> str | None:
...
# Python 3.9 μ΄ν
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
...# μ€μ : κ²μ κ²°κ³Όκ° μμ μ μλ κ²½μ°
def search(query: str) -> list[str] | None:
results = db.search(query)
return results if results else None
# κΈ°λ³Έκ°μ΄ NoneμΈ μΈμ
def connect(host: str, port: int = 5432, password: str | None = None) -> None:
...Optional vs Union
Optional[str]μ μ ννstr | Noneμ΄λ€. βμ νμ βμ΄λΌλ λ»μ΄ μλλΌ Noneμ΄ λ μ μλ€λ λ»μ΄λ€.# μ΄ λμ λμΌ def f(x: Optional[str]) -> None: ... def f(x: str | None) -> None: ...Python 3.10 μ΄μμ΄λ©΄
|λ¬Έλ²μ΄ λ μ§κ΄μ μ΄λ―λ‘Optionalλμstr | Noneμ μ°λ κ²μ΄ κΆμ₯λλ€.
5. Any
νμ μ μ ννκ³ μΆμ§ μμ λ μ¬μ©νλ€. λͺ¨λ νμ κ³Ό νΈνλλ€.
from typing import Any
def log(value: Any) -> None:
print(value)
data: Any = "hello"
data = 123 # OK
data = [1, 2] # OK# dictμ κ°μ΄ λλ λ μ μλ κ²½μ°
config: dict[str, Any] = {
"host": "localhost",
"port": 5432,
"debug": True,
"tags": ["a", "b"],
}Any vs object
Anyλ νμ 체ν¬λ₯Ό μμ ν λλ€.objectλ λͺ¨λ νμ μ λΆλͺ¨μ΄μ§λ§ ν΄λΉ νμ μ λ©μλλ₯Ό μΈ μ μλ€.def f(x: Any) -> None: x.foo() # νμ μ²΄μ»€κ° λ¬΄μ β μλ¬ μ μ‘ν def g(x: object) -> None: x.foo() # νμ 체컀 μλ¬ β objectμ fooκ° μμΌλ―λ‘κ°λ₯νλ©΄
Anyλμ ꡬ체μ μΈ νμ μ μ°λ κ²μ΄ μ’λ€.
6. Callable
ν¨μλ₯Ό μΈμλ‘ λ°κ±°λ λ°νν λ μ¬μ©νλ€.
from collections.abc import Callable
# "strμ λ°μμ intλ₯Ό λ°ννλ ν¨μ"λ₯Ό μΈμλ‘ λ°λλ€
def apply(func: Callable[[str], int], value: str) -> int:
return func(value)
result = apply(len, "hello") # 5
result = apply(int, "42") # 42# μΈμ μμ΄ strμ λ°ννλ ν¨μ
def run(factory: Callable[[], str]) -> str:
return factory()
# μ¬λ¬ μΈμλ₯Ό λ°λ ν¨μ
def on_event(callback: Callable[[str, int], None]) -> None:
callback("click", 42)# μΈμ νμ
μ μ ννμ§ μμ λ
from typing import Any
loose: Callable[..., str] # μ무 μΈμλ λ°κ³ strμ λ°ν# μ€μ : λ°μ½λ μ΄ν° νμ
ννΈ
from collections.abc import Callable
from functools import wraps
def retry(max_attempts: int) -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception:
if i == max_attempts - 1:
raise
return wrapper
return decorator7. Literal and Final
Literal β νΉμ κ°λ§ νμ©
from typing import Literal
def open_file(path: str, mode: Literal["r", "w", "a"]) -> None:
...
open_file("data.txt", "r") # OK
open_file("data.txt", "x") # νμ
체컀 μλ¬# μ€μ : λ°©ν₯, μν λ± μ νλ κ°
def move(direction: Literal["up", "down", "left", "right"]) -> None:
...
def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
...Literal vs Enum
λ λ€ κ°μ μ ννλ μ©λμ§λ§ μ°μμκ° λ€λ₯΄λ€.
Literalβ νμ ννΈ μ μ©, λ°νμμ ν¨κ³Ό μμ, κ°λ¨ν κ²½μ°μ μ ν©Enumβ λ°νμμλ λμ, λ©μλ μΆκ° κ°λ₯, 볡μ‘ν κ²½μ°μ μ ν©
Final β μ¬ν λΉ κΈμ§
from typing import Final
MAX_RETRIES: Final = 3
MAX_RETRIES = 5 # νμ
체컀 μλ¬
API_URL: Final[str] = "https://api.example.com"# ν΄λμ€μμ μμ μ μΈ
class Config:
TIMEOUT: Final[int] = 30
BASE_URL: Final[str] = "https://api.example.com"
# μμ μ μ€λ²λΌμ΄λ λΆκ°
class CustomConfig(Config):
TIMEOUT = 60 # νμ
체컀 μλ¬8. TypeAlias and NewType
TypeAlias β κΈ΄ νμ μ μ΄λ¦ λΆμ΄κΈ°
νμ μ΄ λ³΅μ‘ν΄μ§λ©΄ λ³μΉμ λ§λ€μ΄ κ°λ μ±μ λμΌ μ μλ€.
# Python 3.12+
type UserId = int
type UserMap = dict[UserId, str]
type Matrix = list[list[float]]
type Handler = Callable[[str, int], None]# Python 3.10~3.11
from typing import TypeAlias
UserId: TypeAlias = int
UserMap: TypeAlias = dict[UserId, str]
Matrix: TypeAlias = list[list[float]]# λ³μΉμ μ¬μ©νλ©΄ ν¨μ μκ·Έλμ²κ° κΉλν΄μ§λ€
def transform(data: Matrix) -> Matrix:
...
def get_user(users: UserMap, uid: UserId) -> str:
return users[uid]NewType β κ°μ νμ μ΄μ§λ§ ꡬλΆνκ³ μΆμ λ
NewTypeμ κΈ°μ‘΄ νμ
κ³Ό λμΌνκ² λμνμ§λ§, νμ
μ²΄μ»€κ° λ³κ°μ νμ
μΌλ‘ μ·¨κΈνλ€.
from typing import NewType
UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)
def get_user(uid: UserId) -> str:
...
user_id = UserId(42)
product_id = ProductId(42)
get_user(user_id) # OK
get_user(product_id) # νμ
체컀 μλ¬ β ProductId != UserId
get_user(42) # νμ
체컀 μλ¬ β int != UserIdλ°νμμλ μ무 ν¨κ³Όκ° μλ€. UserId(42)λ κ·Έλ₯ 42λ₯Ό λ°ννλ€. νμ
체컀λ§μ μν μ₯μΉλ€.
TypeAlias vs NewType
TypeAliasβ κΈ΄ νμ μ μΆμ½.UserIdμintλ μμ ν λμΌνκ² μ·¨κΈNewTypeβ κΈ°μ‘΄ νμ κ³Ό ꡬλΆ.UserIdμintλ λ³κ°μ νμ μΌλ‘ μ·¨κΈ
9. TypedDict
λμ λ리μ ν€λ³λ‘ νμ μ μ§μ ν μ μλ€. API μλ΅μ΄λ μ€μ κ°μ μ€ν€λ§κ° κ³ μ λ dictμ μ μ©νλ€.
from typing import TypedDict
class UserDict(TypedDict):
name: str
age: int
email: str
user: UserDict = {"name": "alice", "age": 30, "email": "alice@example.com"}# μΌλΆ ν€κ° μ νμ μΈ κ²½μ°
class UserDict(TypedDict, total=False):
name: str # μ ν
age: int # μ ν
email: str # μ ν
# νμ + μ ν μ‘°ν© (Python 3.11+)
from typing import Required, NotRequired
class UserDict(TypedDict):
name: str # νμ (κΈ°λ³Έ)
age: int # νμ (κΈ°λ³Έ)
nickname: NotRequired[str] # μ ν# μ€μ : API μλ΅ νμ
μ μ
class ApiResponse(TypedDict):
status: int
data: list[dict[str, str]]
message: str
def handle_response(resp: ApiResponse) -> None:
if resp["status"] == 200:
for item in resp["data"]:
print(item)TypedDict vs dataclass
λ λ€ κ΅¬μ‘°νλ λ°μ΄ν°λ₯Ό νννμ§λ§ λ€λ₯΄λ€.
TypedDictβ κ²°κ³Όλ¬Όμ΄ dict κ·Έλλ‘. JSON μ§λ ¬νκ° νμ μκ³ , κΈ°μ‘΄μ dictλ₯Ό μ°λ κ³³μ νμ λ§ μΆκ°νκ³ μΆμ λdataclassβ κ²°κ³Όλ¬Όμ΄ ν΄λμ€ μΈμ€ν΄μ€. λ©μλ μΆκ°,.μ κ·Ό, λΉκ΅ μ°μ° λ±μ΄ νμν λ
10. Protocol
βμ΄ λ©μλλ₯Ό κ°μ§κ³ μμΌλ©΄ μ΄ νμ μΌλ‘ μΈμ νλ€βλ ꡬ쑰μ νμ΄ν(Structural Typing) μ ꡬννλ€. Javaμ μΈν°νμ΄μ€μ λΉμ·νμ§λ§, λͺ μμ μΌλ‘ μμνμ§ μμλ λλ€.
from typing import Protocol
class Closable(Protocol):
def close(self) -> None:
...
# μ΄ ν¨μλ close() λ©μλκ° μλ κ°μ²΄λ©΄ λλ λ°λλ€
def cleanup(resource: Closable) -> None:
resource.close()# νμΌ κ°μ²΄ β close()κ° μμΌλ Closable
f = open("test.txt")
cleanup(f) # OK
# 컀μ€ν
ν΄λμ€ β close()κ° μμΌλ Closable (μμ λΆνμ)
class DatabaseConnection:
def close(self) -> None:
print("DB μ°κ²° μ’
λ£")
cleanup(DatabaseConnection()) # OK
# close()κ° μλ ν΄λμ€
class PlainObject:
pass
cleanup(PlainObject()) # νμ
체컀 μλ¬# μ€μ : μ¬λ¬ λ©μλλ₯Ό μꡬνλ Protocol
class Readable(Protocol):
def read(self, size: int = -1) -> str:
...
def readline(self) -> str:
...
def process(source: Readable) -> list[str]:
lines = []
while line := source.readline():
lines.append(line)
return linesProtocol vs ABC(Abstract Base Class)
ABCβ μμμ΄ νμ.class MyFile(ABC):μ²λΌ λͺ μμ μΌλ‘ μμν΄μΌ νλ€Protocolβ μμ λΆνμ. λ©μλ μκ·Έλμ²λ§ λ§μΌλ©΄ λλ€ (duck typingμ μ μ λ²μ )
11. TypeVar and Generic
ν¨μλ ν΄λμ€κ° μ¬λ¬ νμ μ λν΄ λμΌνκ² λμν λ, νμ λ³μλ₯Ό μ¬μ©ν΄ βμ λ ₯ νμ κ³Ό μΆλ ₯ νμ μ κ΄κ³βλ₯Ό νννλ€.
TypeVar
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
# νμ
μ²΄μ»€κ° μΆλ‘ :
first([1, 2, 3]) # T = int β λ°ν νμ
int
first(["a", "b"]) # T = str β λ°ν νμ
strTκ° μμΌλ©΄ μ΄λ κ² μΈ μλ°μ μλ€:
# Anyλ₯Ό μ°λ©΄ λ°ν νμ
μ λ³΄κ° μ¬λΌμ§λ€
def first(items: list[Any]) -> Any:
return items[0]
result = first([1, 2, 3]) # resultμ νμ
: Any (intκ° μλ)Bound β μν μ μ½
T = TypeVar("T", bound=str)
# Tλ str λλ strμ μλΈν΄λμ€λ§ κ°λ₯
def to_upper(s: T) -> T:
return s.upper()Python 3.12+ λ¬Έλ²
Python 3.12λΆν°λ TypeVarλ₯Ό λ³λλ‘ μ μΈνμ§ μμλ λλ€.
# Python 3.12+
def first[T](items: list[T]) -> T:
return items[0]
# ν΄λμ€μλ μ¬μ© κ°λ₯
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
stack = Stack[int]()
stack.push(1) # OK
stack.push("a") # νμ
체컀 μλ¬12. Version changes
| λ²μ | μ£Όμ λ³κ²½ |
|---|---|
| 3.5 | typing λͺ¨λ λμ
|
| 3.9 | λ΄μ₯ μ λ€λ¦ (list[int], dict[str, int]). typing.List λ± λΆνμ |
| 3.10 | X | Y union λ¬Έλ². Optional[X] λμ X | None |
| 3.11 | TypedDictμ Required, NotRequired μΆκ° |
| 3.12 | type λ¬Έ, μ λ€λ¦ λ¬Έλ² def f[T](), class C[T]: |
μ΄λ€ λ²μ λ¬Έλ²μ μΈκΉ
- Python 3.10+ β
int | str,str | Noneμ¬μ© (κ°μ₯ κΉλ)- Python 3.9 β
list[int]μ¬μ©, Unionμtyping.Union- Python 3.8 μ΄ν β
typing.List[int],typing.Optional[str]
from __future__ import annotationsλ₯Ό νμΌ λ§¨ μμ μ°λ©΄ 3.7~3.9μμλlist[int],int | strλ¬Έλ²μ μ¬μ©ν μ μλ€. (λ¬Έμμ΄λ‘ νκ°κ° μ§μ°λλ λ°©μ)
13. Practical examples
μ§κΈκΉμ§ λ°°μ΄ κ²λ€μ μ‘°ν©ν μ€μ μμλ€μ΄λ€.
API client
from typing import Any, TypedDict
class ApiResponse(TypedDict):
status: int
data: dict[str, Any]
error: str | None
def fetch(url: str, params: dict[str, str] | None = None) -> ApiResponse:
...
def extract_field(response: ApiResponse, field: str) -> Any:
return response["data"].get(field)Data pipeline
from collections.abc import Callable, Iterable
type Transform = Callable[[dict[str, Any]], dict[str, Any]]
def pipeline(
data: Iterable[dict[str, Any]],
transforms: list[Transform],
) -> list[dict[str, Any]]:
results = list(data)
for transform in transforms:
results = [transform(item) for item in results]
return results
# μ¬μ©
def add_timestamp(record: dict[str, Any]) -> dict[str, Any]:
record["processed_at"] = "2026-04-13"
return record
def normalize_keys(record: dict[str, Any]) -> dict[str, Any]:
return {k.lower(): v for k, v in record.items()}
output = pipeline(raw_data, [normalize_keys, add_timestamp])Config with defaults
from dataclasses import dataclass, field
from typing import Final, Literal
LOG_LEVELS = Literal["DEBUG", "INFO", "WARNING", "ERROR"]
@dataclass
class AppConfig:
host: str = "localhost"
port: int = 8080
debug: bool = False
log_level: LOG_LEVELS = "INFO"
allowed_origins: list[str] = field(default_factory=list)
MAX_CONNECTIONS: Final[int] = 100Repository pattern
from typing import TypeVar, Protocol
class HasId(Protocol):
id: int
T = TypeVar("T", bound=HasId)
class Repository:
def __init__(self) -> None:
self._store: dict[int, HasId] = {}
def save(self, entity: T) -> T:
self._store[entity.id] = entity
return entity
def find(self, entity_id: int) -> HasId | None:
return self._store.get(entity_id)