TechFeedTechFeed
Programming Languages

파이썬 타입힌트, pyright, mypy, pyproject | 머지 전에 어떤 검사를 켜나?

파이썬 타입힌트를 파이라이트와 마이파이로 검사하기 전에 pyproject 설정, 엄격 모드 단계, py.typed 스텁, CI 게이트를 체크리스트로 본다. 레거시에 한 번에 엄격 모드를 켜면 깨지는 이유와 ignore 만료, uv 환경, 의존성 스텁, 개발자, 백엔드, API, 데이터, Python 스크립트 머지 전에 채울 항목.

by

CI는 초록인데 스테이징에서 속성 오류가 나면, 테스트가 빠진 게 아니라 타입 검사가 빠져 있는 경우가 많다. 파이썬은 실행 전에 시그니처를 강제하지 않아서, 힌트를 달아도 검사기가 돌지 않으면 주석과 같다. 머지 전에 켤 항목은 검사기 하나 고정, 설정 파일에 모드와 제외 경로 기록, 풀 리퀘스트에서 검사가 실패하면 막기 세 가지다. 에디터 밑줄만 믿고 넘어가면 사람마다 통과 기준이 갈린다. 한글 변수와 주석이 많은 스크립트일수록, 로컬에서만 초록인 상태를 팀 규칙으로 착각하기 쉽다.


패키지 설치와 가상환경은 유브이 패키지 매니저가상환경 가이드를, 의존성 갱신은 디펜다봇, 리노베이트를 같이 보면 빈칸이 줄어든다. 프론트에서 스키마로 런타임 타입을 거는 흐름은 타입스크립트 6 마이그레이션과 결이 비슷하다.


검사기를 고르는 순서 | 파이라이트냐 마이파이냐

새 저장소는 파이라이트(Pyright)를 기본 게이트로 두고, 이미 마이파이(mypy) 설정이 있는 저장소는 마이파이를 유지한 채 규칙을 조이는 편이 싸다. 두 도구를 같은 PR에서 동시에 엄격 모드로 켜면 같은 줄을 서로 다른 이유로 막는다.


파이라이트는 마이크로소프트가 만든 정적 검사기고, 브이에스코드 파이랜스(Pylance)의 엔진이다. 공식 문서는 모드를 off, basic, standard, strict 네 단계로 나눈다. 마이파이는 파이썬 타이핑 명세의 참고 구현에 가깝고, 플러그인과 모듈별 오버라이드가 길다. 아스트랄의 티와이(ty)는 러스트로 만든 빠른 검사기고 uvx ty check로 돌려 볼 수 있다. 속도 후보이지만, 기존 게이트를 하루아침에 갈아끼우기 전에 같은 커밋에서 결과 차이를 표로 남겨 두는 편이 안전하다.


도구잘 맞는 자리설정 칸주의
파이라이트새 프로젝트, 편집기 피드백과 CI를 맞출 때[tool.pyright] 또는 pyrightconfig.json모드 기본값이 팀마다 다름. 파일에 명시
마이파이이미 플러그인·모듈 예외가 쌓인 레포[tool.mypy], mypy.ini주석 없는 함수 본문은 기본으로 안 봄
티와이큰 트리에서 검사 시간이 병목일 때 병행공식 문서의 프로젝트 설정기존 게이트와 진단이 다를 수 있음

도구 비교의 원문은 파이라이트 쪽 마이파이 비교 문서와 마이파이 공식 문서다. 티와이 설치·실행은 docs.astral.sh/ty를 기준으로 본다. 숫자는 벤치 홍보 문구를 그대로 옮기지 말고, 자기 저장소에서 한 번 잰 시간을 적는다.


설정을 한 파일에 모으는 이유 | pyproject 칸

검사 옵션을 커맨드라인에만 두면, 로컬은 느슨하고 CI만 빡세진다. pyproject.toml에 모드·포함 경로·제외 경로·파이썬 버전을 적으면 명령은 짧아지고 리뷰가 쉬워진다.


파이라이트는 [tool.pyright] 또는 pyrightconfig.json을 읽는다. 마이파이는 [tool.mypy], mypy.ini, .mypy.ini, setup.cfg 순으로 찾는다. 같은 저장소에 두 칸이 같이 있어도 된다. 다만 include/exclude가 어긋나면 “로컬은 통과, CI는 다른 파일을 본다”가 난다.


pyproject.toml | 파이라이트 표준 모드 예시
[tool.pyright] include = ["src", "scripts"] exclude = ["**/__pycache__", "**/.venv", "**/node_modules", "dist"] pythonVersion = "3.12" typeCheckingMode = "standard" venvPath = "." venv = ".venv"
pyproject.toml | 마이파이 점진 도입 예시
[tool.mypy] python_version = "3.12" packages = ["src"] warn_unused_ignores = true warn_return_any = true check_untyped_defs = true disallow_untyped_defs = false no_implicit_optional = true pretty = true [[tool.mypy.overrides]] module = ["boto3.*", "botocore.*"] ignore_missing_imports = true

판정 규칙. CI 명령은 플래그를 최소화한다. pyright 또는 mypy만 두고, 예외는 파일에 적는다. “내 노트북에선 이 플래그로 통과”를 허용하지 않는다.


파이썬 프로젝트 설정 파일과 타입 검사 구성이 열린 개발 화면
모드와 제외 경로는 명령줄이 아니라 설정 파일에 고정한다

한 번에 엄격 모드를 켜면 깨지는 이유

레거시에 strict를 하루 만에 켜면 수천 줄이 빨개지고, 팀은 검사를 끄거나 무시 주석을 남발한다. 마이파이 문서도 기존 코드에는 플래그를 하나씩 올리라고 적는다.


마이파이 기본값은 주석이 없는 함수 본문을 거의 보지 않는다. check_untyped_defs를 켜야 몸통을 본다. disallow_untyped_defs는 시그니처 자체를 강제한다. 순서를 뒤집으면 “아직 손도 안 댄 모듈”이 전부 실패한다. 파이라이트는 모드를 basic → standard → strict로 올리되, 패키지 단위 오버라이드로 옛 폴더는 한 단계 낮게 둘 수 있다.


단계먼저 켤 것아직 끄지 말 것
1주 차포함 경로, 파이썬 버전, 미사용 ignore 경고전 저장소 시그니처 강제
2~3주 차새 파일만 시그니처 필수, 본문 검사서드파티 전부 스텁 없음 에러
안정 후핵심 패키지 strict, Any 유입 제한벤더·생성 코드까지 같은 모드

스텁이 없는 패키지를 만났을 때 | py.typed

라이브러리를 가져왔는데 “타입 정보가 없다”고 하면, 먼저 그 패키지가 펩 561을 지키는지 본다. 배포물에 빈 파일 py.typed가 있으면 소스의 힌트를 신뢰해도 된다는 표시다.


표시가 없으면 검사기는 그 모듈을 느슨하게 보거나, 설정에 따라 에러를 낸다. 마이파이 --install-types는 타입이 검증된 스텁 패키지 일부만 설치한다. 아무 패키지나 받아 실행하지 않는다. 아마존 웹 서비스 같은 거대 SDK는 boto3-stubs처럼 별도 스텁을 쓰는 팀이 많다. 국내에서만 쓰는 사내 패키지는 스텁이 없는 경우가 흔하니, 오버라이드로 잠시 숨기지 말고 패키지 저장소에 py.typed를 넣는 쪽을 먼저 요청한다.


의존성 자체를 올리는 주기는 의존성 자동 업데이트 체크리스트와 맞춘다. 스텁만 올리고 본 패키지는 그대로 두면, 런타임 API와 힌트가 어긋난다.


파이썬 패키지와 타입 힌트가 표시된 코드 리뷰 화면
스텁과 본 패키지 버전이 어긋나면 검사는 통과하고 런타임만 깨진다

CI에 넣을 명령 | PR에서 막을 세 줄

검사는 로컬 pre-commit과 CI 둘 다에서 같은 버전이 돌아야 한다. 버전을 안 고정하면, 한 사람은 1.1 기준으로 통과하고 다른 사람은 다음 마이너에서 막힌다.


캐시는 속도를 위해 남겨도 된다. 마이파이는 .mypy_cache를 쓰고, 파이라이트도 증분 결과를 남긴다. 캐시 디렉터리는 아티팩트로 올리되, 파이썬 버전이나 설정 파일이 바뀌면 캐시를 버린다. 린트(러프)와 타입 검사를 한 잡에 섞으면 실패 원인이 안 보인다. 잡 이름을 나눈다.


같은 명령을 로컬과 CI에 고정
# 파이라이트 게이트 uv run pyright # 마이파이 게이트 uv run mypy # 티와이 병행(기존 게이트를 대체하지 않음) uvx ty check

무시 주석은 언제까지 남기나

# type: ignore는 빚이다. 코드 없이 달면 그 줄의 다른 오류까지 삼킨다. 마이파이는 # type: ignore[attr-defined]처럼 코드를 붙이는 쪽을 권한다. 파이라이트도 규칙 단위 억제가 있다.


무시가 세 달 이상 남아 있으면, 이슈 번호와 담당이 없는 항목부터 지운다. 라이브러리 스텁이 생긴 뒤에는 warn_unused_ignores가 죽은 주석을 찾아 준다. “일단 머지하고 다음에”가 반복되면 설정 파일의 전역 ignore 목록이 늘어나고, 검사는 형식만 남는다.


오늘 채울 순서 | 한 저장소 반나절

도구 비교를 더 읽기 전에, 지금 쓰는 저장소에 아래 순서로 칸을 채운다. 반나절이면 게이트가 생긴다.


  1. 런타임 파이썬 버전을 확인하고 설정 파일에 같은 숫자를 적는다.
  2. 검사기 하나를 고른다. 새 레포는 파이라이트 standard, 기존 마이파이 레포는 마이파이 유지.
  3. 포함 경로를 src 또는 실제 패키지 이름으로 좁힌다. 테스트·스크립트는 다음 주.
  4. 로컬에서 한 번 돌려 에러 수를 이슈에 적는다. 목표 숫자는 “이번 주 0”이 아니라 “신규 파일 0 증가”다.
  5. 같은 명령을 CI 필수 검사로 올린다.
  6. 다음 스프린트에 레거시 한 패키지만 모드를 한 단계 올린다.

프론트 번들 예산을 PR에서 막는 것과 같은 습관이다. 성능 쪽은 성능 예산 체크리스트를 보면 게이트를 나누는 감이 온다. 설치 자체가 안 된 환경이면 파이썬 설치 가이드부터 맞춘다.


풀 리퀘스트 검사 게이트가 타입 오류로 실패한 연속 통합 화면
로컬과 CI가 같은 명령을 돌려야 밑줄과 머지 버튼이 같은 말을 한다

참고 자료


내부 연계: 유브이, 가상환경, 의존성 업데이트, 타입스크립트 6, 시크릿


자주 묻는 질문

파이라이트와 마이파이 중 하나만 쓰나, 둘 다 쓰나?

게이트는 하나만 둔다. 새 저장소는 파이라이트, 이미 마이파이 예외가 쌓인 저장소는 마이파이를 유지한다. 둘을 같은 PR에서 엄격 모드로 켜면 같은 줄을 서로 다른 규칙으로 막아 리뷰가 길어진다. 편집기 엔진과 CI 도구가 달라도 된다. 다만 CI가 통과인데 편집기만 빨개지면, 모드와 제외 경로가 어긋난 것이다. 설정 파일을 한곳으로 모은 뒤 명령을 같게 맞춘다.


티와이로 마이파이를 바로 바꿔도 되나?

병행은 괜찮고, 교체는 결과 표를 본 뒤에 한다. 티와이는 아스트랄 문서 기준으로 빠르고 언어 서버도 제공한다. 진단 규칙과 엄격함은 기존 도구와 다를 수 있다. 같은 커밋에서 두 명령을 돌리고, 막히는 줄이 진짜 버그인지 규칙 차이인지 칸을 나눈다. 기존 게이트를 끄기 전에 한 스프린트 정도 숫자를 본다.


주석이 없는 옛 함수도 바로 막아야 하나?

아니다. 마이파이는 기본으로 그 본문을 거의 보지 않는다. 먼저 포함 경로와 버전을 고정하고, 새 파일만 시그니처를 강제한다. 그다음 check_untyped_defs로 몸통을 보고, 마지막에 시그니처 강제를 핵심 패키지로 넓힌다. 한 번에 전 저장소를 막으면 무시 주석만 늘어난다.


서드파티에서 타입 정보 없음이 뜬다. 전역으로 끌까?

전역 ignore_missing_imports는 쓰지 않는다. 그 모듈만 오버라이드하거나, 공식 스텁·py.typed를 찾는다. 사내 패키지면 배포물에 빈 표시 파일을 넣는 쪽이 싸다. 전역으로 끄면 오타 임포트도 통과한다.


테스트 파일은 검사를 빼도 되나?

픽스처와 모킹이 많은 테스트는 한 단계 낮게 둘 수 있다. 빼는 이유를 오버라이드에 적는다. 테스트에서만 Any를 남발하면, 프로덕션 함수 시그니처가 틀려도 호출부가 침묵한다. 헬퍼가 프로덕션과 공유되면 그 헬퍼는 본 규칙으로 둔다.


타입 검사가 있으면 단위 테스트는 줄여도 되나?

아니다. 검사기는 값과 부수 효과를 실행하지 않는다. 잘못된 할인율, 빈 리스트, 네트워크 타임아웃은 테스트와 모니터링 몫이다. 힌트는 “이 함수가 문자열을 받는지”를 막고, 테스트는 “그 문자열이 주문번호 형식인지”를 막는다. 둘 중 하나를 끄면 구멍이 남는다.


파이썬타입힌트pyrightmypyPython개발자백엔드API데이터SDK

함께 보면 좋은 문제 해결

EXPLORE / Programming Languages

이어서 읽어보기

전체 토픽 둘러보기