포스트

PEP 8, 어디까지 지켜야 하나: 코드 리뷰에서 자주 걸리는 12가지 규칙

PEP 8의 목적은 모든 코드를 기계적으로 똑같이 만드는 것이 아니라 팀원이 다시 읽을 때 해석 비용을 줄이는 것이다. 규칙이 충돌할 때도 핵심 기준은 일관성과 가독성이다.

어떤 규칙부터 코드 리뷰에 적용할까?

따라서 리뷰는 공백 하나를 지적하는 데서 끝나지 않는다. 도구로 고칠 형식과 사람이 판단할 이름, 책임, 제어 흐름을 나누고, 기존 코드와 충돌하는 변경은 팀 규칙으로 합의해야 한다.

공식 원문은 PEP 8 Style Guide for Python Code에서 확인할 수 있다. 아래는 긴 문서를 코드 리뷰에서 자주 마주치는 판단 기준으로 압축한 내용이다.

들여쓰기와 줄바꿈: 모양보다 구조가 먼저다

기본 들여쓰기는 공백 4칸을 사용하고 탭과 공백을 섞지 않는다. 함수 호출이 길어질 때는 여는 괄호에 맞추거나 hanging indent를 쓴다.

1
2
3
4
5
6
7
8
9
10
11
# 여는 괄호에 맞춘 수직 정렬
result = long_function_name(var_one, var_two,
                            var_three, var_four)

# hanging indent
result = long_function_name(
    var_one,
    var_two,
    var_three,
    var_four,
)

함수 정의의 인자와 본문이 같은 깊이로 보여 혼동될 때는 인자에 한 단계 더 들여쓰기를 준다.

1
2
3
4
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

PEP 8은 코드 한 줄을 79자, 주석과 docstring을 72자로 제한하는 방향을 제시한다. 숫자를 외우기보다 diff와 화면에서 한 줄을 무리 없이 읽을 수 있게 끊는 것이 실용적인 판단 기준이다. 이항 연산자는 연산자 에서 줄을 바꾸면 계산 흐름이 잘 보인다.

1
2
3
4
5
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

닫는 괄호는 마지막 원소 다음 줄에 둘 수 있다. 여러 줄 자료구조에는 trailing comma를 남기면 항목 추가 시 수정 범위도 작아진다.

1
2
3
4
files = [
    'setup.cfg',
    'tox.ini',
]

import와 공백: 검색 가능한 패턴을 만든다

import는 한 줄에 하나씩 쓰고 다음 세 그룹 사이를 빈 줄로 나눈다.

  1. standard library
  2. third-party library
  3. local application/library
1
2
3
4
5
6
import os
import sys

from PIL import Image

from mypkg.sibling import example

절대 경로 import가 기본이며, 같은 패키지 안에서 불필요하게 긴 경로를 피할 때 명시적 상대 경로를 사용할 수 있다. __all__, __version__ 같은 module-level dunder 이름은 module docstring과 __future__ import 뒤, 일반 import 앞에 둔다.

공백은 장식이 아니라 문법 단위를 보여준다. 괄호 안쪽, 함수 이름과 여는 괄호 사이, 인덱싱 괄호 앞에는 공백을 넣지 않는다.

1
2
spam(ham[1], {'eggs': 2})
dct['key'] = values[index]

할당과 비교 연산자 주변에는 한 칸을 두되, 함수 호출의 keyword 인자와 기본값에는 붙여 쓴다. 타입 표기가 함께 있으면 기본값 주변에 공백을 둔다.

1
2
3
4
5
x = 1
result = transform(value, limit=1000)

def transform(value: str, limit: int = 1000) -> str:
    ...

슬라이스의 콜론은 이항 연산자처럼 양쪽 간격을 맞춘다. 식이 복잡하다면 한 줄에 억지로 넣는 대신 중간 변수로 뜻을 드러내는 편이 낫다.

이름과 주석: 코드가 이미 말하는 것은 반복하지 않는다

이름만 보고 역할을 추측할 수 있어야 한다.

  • 클래스와 예외: ClassName, ExceptionName
  • 함수, 메서드, 변수: lower_case_with_underscores
  • 상수: UPPER_CASE_WITH_UNDERSCORES
  • 모듈, 패키지: 짧은 소문자 이름

l, O, I는 글꼴에 따라 숫자 1이나 0과 헷갈리므로 한 글자 변수로 피한다. 약어가 들어간 클래스 이름은 프로젝트 안에서 한 방식을 정해 일관되게 쓴다.

좋은 주석은 코드가 무엇을 하는지 되풀이하지 않고 그렇게 했는지를 설명한다.

1
2
# 경계 픽셀을 포함하기 위해 1을 더한다.
width = right - left + 1

공개 모듈, 함수, 클래스, 메서드에는 docstring을 작성한다. 주석이 코드와 달라졌다면 오래된 주석은 설명이 아니라 버그가 된다. 코드 변경과 함께 갱신할 수 없는 주석은 남기지 않는 편이 낫다.

조건문과 예외 처리: 짧은 코드보다 오해 없는 코드

Python의 관용적 표현은 의도를 바로 드러낸다.

1
2
3
4
5
6
7
8
9
10
11
if value is not None:
    use(value)

if items:
    process(items)

if isinstance(obj, int):
    handle_integer(obj)

if filename.endswith('.json'):
    load_json(filename)

if not value is None, len(items) != 0, type(obj) is type(1), 문자열 슬라이싱으로 접미사를 비교하는 표현보다 위 코드가 읽기 쉽다. bool 값도 if enabled:처럼 직접 검사한다.

간단한 함수라도 이름이 필요하다면 lambda를 변수에 대입하기보다 def를 쓴다. traceback과 함수 이름이 분명해진다.

1
2
def double(value):
    return 2 * value

try 블록은 예외가 발생할 수 있는 최소 범위로 좁힌다. 그래야 처리하려던 KeyError와 후속 함수 내부의 KeyError를 구분할 수 있다.

1
2
3
4
5
6
try:
    value = collection[key]
except KeyError:
    return key_not_found(key)
else:
    return handle_value(value)

반환값도 모든 경로에서 같은 의미를 유지한다. 한 경로가 값을 반환한다면 값이 없는 경로에서는 return None을 명시해 독자가 빠르게 판단할 수 있게 한다.

마지막 주의점은 PEP 8을 목적보다 앞세우지 않는 것이다. 기존 프로젝트의 일관된 규칙, 외부 API와의 호환성, 읽기 쉬운 예외가 있다면 무조건 형태를 바꾸지 않는다. 스타일 리뷰의 좋은 질문은 “규칙 번호가 무엇인가?”보다 “이 표현을 처음 보는 사람이 잘못 읽을 가능성이 줄었는가?”다.

코드 리뷰에서 형식과 설계를 어떻게 나누나

첫 단계는 formatter와 linter가 자동으로 찾을 수 있는 항목이다. 들여쓰기, trailing whitespace, import 순서, 일부 줄 길이 문제는 도구 결과를 기준으로 처리한다. 사람이 매번 같은 공백을 토론하지 않도록 실행 명령과 설정 파일을 프로젝트에 고정한다.

두 번째는 이름과 범위다. 변수 이름이 값의 단위와 의미를 드러내는지, 함수 이름이 실제 side effect를 숨기지 않는지 본다. 짧은 loop index와 넓은 범위에서 재사용되는 상태 변수에 같은 이름 기준을 강제하지 않고, 잘못 읽을 비용으로 판단한다.

세 번째는 조건과 예외다. None 확인과 truthiness가 같은 의미인지, 넓은 exception이 실제 오류를 숨기는지, 여러 return 경로가 같은 타입과 의미를 갖는지 확인한다. 문법상 짧은 표현이 업무 규칙을 숨긴다면 명시적인 분기가 더 읽기 쉽다.

규칙을 적용하는 안전한 순서

형식 변경과 동작 변경을 한 commit에 섞지 않는다. 먼저 테스트를 통과하는 상태에서 formatter만 적용하고, 그다음 이름 변경이나 함수 분리를 별도 diff로 만든다. 그렇지 않으면 실제 로직 차이가 대량의 공백 변경 안에 숨는다.

기존 공개 API의 이름은 PEP 8에 맞지 않더라도 호환 비용이 있을 수 있다. 내부 alias나 점진적 deprecation이 필요한지 판단하고 일괄 변경하지 않는다. 외부 데이터 field도 원문 이름을 보존해야 하는 경우가 있다.

주석은 코드가 이미 말하는 동작을 반복하지 않고 이유와 제약을 설명한다. 오래된 주석이 코드와 다르면 없는 것보다 위험하므로 로직 변경 시 함께 검토한다. TODO에는 무엇이 막혔고 어떤 조건에서 제거할지를 적어야 검색 가능한 작업이 된다.

리뷰 의견을 팀 규칙으로 바꾸는 법

같은 지적이 반복되면 개인 취향으로 남기지 않고 formatter, linter 설정이나 짧은 style guide로 옮긴다. 예외가 필요한 코드에는 왜 기본 규칙보다 읽기 쉬운지 설명한다. 규칙 수를 늘리는 것보다 자동화 가능성과 실제 오해 감소를 기준으로 고른다.

리뷰어는 문제와 제안을 구분한다. “이 이름으로는 단위를 오해할 수 있다”는 문제를 먼저 쓰고 대안 이름을 제시한다. 작성자도 규칙 번호보다 주변 코드와 API 제약을 근거로 답하면 일관된 결정을 남길 수 있다.

자동 formatter와 linter의 역할도 구분해야 합니다. Formatter는 줄바꿈과 공백처럼 기계적으로 결정할 수 있는 모양을 일관되게 만들고, linter는 사용하지 않는 import나 잠재적 오류 패턴을 알립니다. 그러나 변수 이름이 도메인 의미를 드러내는지, 함수가 한 책임만 갖는지, 예외를 삼키는지는 도구만으로 결정하기 어렵습니다. 리뷰에서는 자동화 가능한 지적을 먼저 없애 사람이 설계와 의미에 집중하도록 합니다.

기존 프로젝트에 규칙을 도입할 때 전체 파일을 한꺼번에 다시 포맷하면 기능 변경의 diff가 묻힐 수 있습니다. 포맷 전용 변경을 별도로 분리하거나 수정하는 파일부터 점진적으로 적용하고, CI에서 같은 버전과 설정을 사용합니다. 로컬과 CI의 formatter 버전이 다르면 개발자가 고친 코드가 다시 실패하는 순환이 생깁니다.

예외 규칙은 이유와 범위를 문서로 남깁니다. 긴 URL이나 읽기 쉬운 수식처럼 줄을 억지로 나누는 편이 나쁜 경우가 있지만, “이번에는 보기 싫어서”라는 기준은 팀마다 달라집니다. 코드 가까이에 필요한 최소한의 억제만 두고, 프로젝트 전체 검사를 끄지 않아야 새 오류가 조용히 늘지 않습니다.

함께 읽으면 이해가 이어지는 글

자주 묻는 질문

PEP 8은 모든 규칙을 예외 없이 지켜야 하나요?

아닙니다. 목적은 가독성과 일관성입니다. 기존 프로젝트 규칙, 외부 API 호환, 더 읽기 쉬운 표현이 있다면 이유를 남기고 일관되게 예외를 적용할 수 있습니다.

자동 formatter를 쓰면 PEP 8 리뷰가 끝나나요?

끝나지 않습니다. Formatter는 공백과 줄바꿈 같은 형식을 많이 처리하지만 이름의 의미, 함수 책임, 오해하기 쉬운 조건과 주석 품질은 사람이 판단해야 합니다.

긴 줄은 무조건 줄 수만 줄여야 하나요?

단순히 아무 위치에서 자르기보다 괄호 안의 논리 단위와 연산 우선순위가 드러나게 나눕니다. 한 줄을 유지하는 것보다 변경 diff와 구조를 읽기 쉬운지가 중요합니다.

THE END / OPSOAI

여기까지 읽었습니다

핵심 장면을 한 번 더 떠올려 보세요. 이해가 남았다면 이 책은 제 역할을 다했습니다.

다른 책 고르기
표지 1

키와 좌우 스와이프를 지원합니다. 읽던 페이지는 이 기기에 저장됩니다.

CONTENTS

이 책의 목차

    10개 장 18 분읽는 시간