3 분 소요

MyPy 소개

MyPy는 Python 코드에 타입 힌트를 추가하여 정적 타입 검사를 수행할 수 있게 해주는 도구입니다. Python은 본래 동적 타입 언어로, 실행 중에 변수의 타입이 결정됩니다. 하지만 프로젝트 규모가 커지거나 코드 복잡도가 증가하면 타입 관련 오류가 발생하기 쉽습니다. MyPy는 이러한 문제를 사전에 방지하기 위해 개발되었습니다.

MyPy의 주요 기능

  • 정적 타입 검사: 코드 작성 시점에 변수의 타입을 미리 검사하여 타입 오류를 찾아냅니다.
  • 타입 힌트 지원: Python의 타입 힌트(annotation)를 활용하여 함수의 인자와 반환 값에 대한 타입을 명시할 수 있습니다.
  • 기존 코드와의 호환성: 기존 Python 코드에 타입 힌트를 추가하는 것만으로도 MyPy를 적용할 수 있으며, 단계적으로 도입할 수 있습니다.
  • 코드 가독성 향상: 타입 힌트를 통해 코드의 의도를 명확히 표현할 수 있어, 유지보수와 협업에 유리합니다.

MyPy 사용 방법

  1. 타입 힌트 추가: 코드에 타입 힌트를 추가합니다.
  2. MyPy 설치: pip install mypyMyPy를 설치합니다.
  3. 타입 검사 실행: mypy your_script.py 명령으로 타입 검사를 실행합니다.

MyPy 적용 중 가장 큰 진입 장벽

MyPy는 Python3 이후 강력한 Lint 로 활용되고 있습니다. 하지만 기존 코드에 MyPy 를 적용하려고 하면 많은 에러가 발생합니다. 이는 기존 코드에 타입 힌트가 없기 때문입니다. 아래와 같은 에러를 만날때는 매우 난감합니다.

❯ mypy .
Found 2598 errors in 1012 file (checked 1526 source files)

그럼 MyPy 적용을 포기해야 할까요?

아닙니다. mypy에서 특정 파일 또는 디렉터리를 검사에서 제외하거나 무시하는 방법이 있습니다. 이를 설정 파일(mypy.ini 또는 setup.cfg)이나 소스 코드에 직접 주석을 추가하여 적용할 수 있습니다.

MyPy 적용할 때 검사해서 회피하는 방법

설정 파일에서 특정 파일/디렉터리 무시하기

mypy의 설정 파일(mypy.ini 또는 setup.cfg)에서 특정 파일이나 디렉터리를 완전히 제외할 수 있습니다.

[mypy]
exclude = (your_regex_here)

[mypy-your_module_to_ignore]
ignore_errors = True

이 방법을 사용하면 특정 파일 또는 모듈 전체를 무시할 수 있습니다. 예를 들어, my_module.py 파일을 무시하려면 다음과 같이 작성할 수 있습니다:

[mypy]
ignore_missing_imports = True
strict_optional = True
no_implicit_reexport = True
warn_redundant_casts = True
warn_unused_ignores = True

[mypy-my_module]
ignore_errors = True

코드 내에서 특정 파일 무시하기

코드의 최상단에 # mypy: ignore-errors 주석을 추가하여 해당 파일 전체에서 mypy 검사를 무시할 수 있습니다.

# mypy: ignore-errors

# 이 파일 전체에서 mypy 검사를 무시합니다.

def some_function():
    # mypy가 이 함수의 타입 오류를 무시합니다.
    pass

이렇게 하면 해당 파일에서 발생하는 모든 타입 검사 오류를 무시할 수 있습니다.

코드의 특정 라인에서 무시하기

코드의 특정 라인에서만 mypy 검사를 무시하려면, 주석 # type: ignore를 사용합니다.

def add(a: int, b: str) -> int:
    return a + b # type: ignore

위 코드에서 bstr 인데 int 와 더하려고 하기 때문에 mypy 가 에러를 발생시킵니다. 이럴 때 # type: ignore 를 사용하면 해당 줄을 무시하게 됩니다.

4. 디렉터리 전체를 무시하기

만약 특정 디렉터리 전체를 mypy 검사에서 제외하고 싶다면, 설정 파일에서 해당 디렉터리의 경로를 exclude 옵션에 추가할 수 있습니다.

[mypy]
ignore_missing_imports = True
exclude = (excluded_directory|another_excluded_directory)

이 방법들은 파일, 특정 라인, 또는 디렉터리 단위로 mypy 검사를 무시하는 데 유용하게 사용할 수 있습니다. 이렇게 하면 필요한 부분만 검사에서 제외하고, 나머지 코드에 대한 타입 검사를 유지할 수 있습니다.

선택의 순간

저는 위의 여러 방법 중에서 특정 라인을 무시하는 방법을 가장 선호합니다. 이 방법은 코드의 특정 부분만 무시할 수 있어, 나머지 코드에 대한 타입 검사를 유지하면서도 필요한 부분만 무시할 수 있기 때문입니다.

그럼 이제 위에 수천개의 에러를 가진 파일들을 모두 수정하면 되겠죠?

특정라인 무시하는 스크립트 작성

시간이 없는 여러분들을 위해 모든 MyPy 에러를 찾아서 무시하는 스크립트를 작성해보겠습니다.

MyPy 에러 결과 받기

이렇게 해서 MyPy 에러를 받아올 수 있습니다.

def run_mypy():
    result = subprocess.run(
        ["mypy", ".", "--show-traceback"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
    )
    return result.stdout

MyPy 에러 파싱하기

MyPy 에러는 이런 패턴으로 나옵니다.

{python_file}.py:{line_number}: error: {error_message}
{python_file}.py:{line_number}: error: {error_message}
{python_file}.py:{line_number}: error: {error_message}

위의 패턴을 참고해서 MyPy 에러를 파싱하여 파일명과 라인 번호를 추출합니다.

def parse_mypy_errors(mypy_output):
    error_pattern = re.compile(r"^(.+):(\d+): error: .+")
    errors = []
    for line in mypy_output.splitlines():
        match = error_pattern.match(line)
        if match:
            filename, line_number = match.groups()
            errors.append((filename, int(line_number)))
    return errors

특정 라인에 # type: ignore 주석 추가하기

def add_type_ignore(filename, line_number):
    path = Path(filename)
    lines = path.read_text().splitlines()
    lines[line_number - 1] += "  # type: ignore"
    path.write_text("\n".join(lines) + "\n")

이렇게 하면 MyPy 에러를 찾아서 특정 라인에 # type: ignore 주석을 추가합니다.

전체 스크립트

import subprocess
import re
from pathlib import Path

def run_mypy():
    result = subprocess.run(
        ["mypy", ".", "--show-traceback"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
    )
    return result.stdout

def parse_mypy_errors(mypy_output):
    error_pattern = re.compile(r"^(.+):(\d+): error: .+")
    errors = []
    for line in mypy_output.splitlines():
        match = error_pattern.match(line)
        if match:
            filename, line_number = match.groups()
            errors.append((filename, int(line_number)))
    return errors

def add_type_ignore(filename, line_number):
    path = Path(filename)
    lines = path.read_text().splitlines()
    lines[line_number - 1] += "  # type: ignore"
    path.write_text("\n".join(lines) + "\n")

def main():
    mypy_output = run_mypy()
    errors = parse_mypy_errors(mypy_output)
    for filename, line_number in errors:
        add_type_ignore(filename, line_number)
        print(f"Added `# type: ignore` to {filename}:{line_number}")

if __name__ == "__main__":
    main()

이렇게 하면 MyPy 에러를 찾아서 특정 라인에 # type: ignore 주석을 추가한 뒤, 이후로 발생하는 MyPy 에러는 수정하시면 됩니다. 그리고 해당 주석을 발견한 Contributor 는 해당 라인을 수정해 나가면 어떨까요?

카테고리: ,

업데이트:

댓글남기기