
Java나 TypeScript를 쓰다가 파이썬으로 넘어오면 묘하게 불안한 순간이 있습니다. "이거 오타 나도 빨간 줄 안 뜨는데… 괜찮은 건가?" 하는 느낌입니다. 실제로 파이썬은 함수 본문 안의 오타나 타입 오류를 실행하기 전까지 거의 잡아주지 않습니다. 파싱 단계에서는 괄호가 안 맞거나 들여쓰기가 틀린 정도의 구조적 오류만 걸러내고, 나머지는 전부 런타임의 몫입니다.
문제는 이 특성 때문에 "배포는 성공했는데 30분 뒤 사용자가 특정 버튼을 눌렀을 때 처음으로 터지는" 버그가 태어난다는 점입니다. 자주 타지 않는 else 분기에 오타가 숨어 있으면, 몇 달 뒤 프로덕션에서 만나게 됩니다.
def calculate_discount(price, user):
if user.is_vip:
return price * 0.7
else:
return pirce * 0.9 # 오타! price → pirce
# 이 파일을 import하는 순간엔 아무 에러도 안 남
# 테스트에서도 VIP 케이스만 돌렸다면 통과해버림
calculate_discount(10000, vip_user) # OK
calculate_discount(10000, normal_user) # 💥 NameError: name 'pirce' is not defined
함수가 import될 때나 정의될 때는 파이썬이 본문 내부를 실질적으로 검사하지 않습니다. pirce라는 이름이 있든 없든 일단 함수 객체는 만들어집니다. 호출되고 그 분기에 실제로 도달해야만 NameError가 터집니다.
문법이 아닌 의미 오류는 파이썬이 잡아주지 않으니, 외부 도구와 습관으로 빈틈을 메워야 합니다.
# 1. 타입 힌트 + 정적 검사기(mypy, pyright)로 사전에 잡기
def calculate_discount(price: int, user: User) -> float:
if user.is_vip:
return price * 0.7
else:
return pirce * 0.9 # mypy가 "name 'pirce' is not defined" 잡아줌
# 2. 모든 분기를 커버하는 테스트 작성
def test_calculate_discount():
assert calculate_discount(10000, vip_user) == 7000
assert calculate_discount(10000, normal_user) == 9000 # else 분기도 반드시 실행
# 3. 린터(ruff, pylint)도 사용하지 않는 변수나 정의 안 된 이름을 잡아줌
핵심은 "파이썬이 알아서 해주겠지"라는 기대를 버리는 것입니다. 언어가 허용하는 유연함의 대가로 오류 탐지 책임이 개발자 쪽으로 넘어와 있다고 생각해야 합니다. 타입 힌트 + 정적 분석기 + 테스트, 이 세트가 사실상 필수입니다.
pirce 같은 오타가 있어도 import 단계에선 조용합니다.FastAPI나 Django 같은 웹 서버에서 특히 치명적입니다. 자주 호출되지 않는 에러 핸들링 분기나 관리자용 엔드포인트에 오타가 숨어 있다가, 진짜로 장애가 터졌을 때 그 분기에 처음 진입하면서 "장애 처리 코드가 장애를 일으키는" 상황이 생기기 때문입니다. CI 파이프라인에 ruff check . && mypy . 두 줄만 추가해도 이런 사고의 상당 부분을 막을 수 있습니다. 특히 데이터 파이프라인처럼 한 번 돌면 몇 시간씩 걸리는 배치 작업에서는, 끝나기 직전 분기에 숨은 오타 하나로 전체를 다시 돌리는 비용이 엄청나기 때문에 사전 검사가 곧 돈입니다.