파이썬 기본 인자에 []를 쓰면 안 되는 이유: 기본값은 함수를 정의할 때 한 번만 만들어진다

def f(a, L=[])를 여러 번 호출하면 리스트가 계속 쌓입니다. 파이썬 공식 문서가 설명하는 기본값 평가 시점, __defaults__, None 패턴과 dataclass의 default_factory까지 예제로 정리했습니다.

개발Python

다음 함수를 세 번 호출하면 무엇이 출력될까요?

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

매번 새 리스트가 만들어진다고 생각하면 [1], [2], [3]을 기대하게 됩니다. 실제 출력은 [1], [1, 2], [1, 2, 3]입니다. 이 예제는 파이썬 공식 튜토리얼에 “중요한 경고”라는 제목으로 그대로 실려 있습니다. 버그가 아니라 언어가 정한 동작입니다.

이 글은 2026년 10월 10일에 확인한 Python 3.15.0 문서를 기준으로 합니다. 여기서 다루는 기본값 평가 규칙은 Python 3 내내 같습니다. 버전에 따라 달라지는 부분은 dataclass 절에 따로 적었습니다.

기본값은 def 문이 실행될 때 평가된다

언어 레퍼런스의 함수 정의 절은 이렇게 정합니다. 기본 매개변수 값은 함수 정의가 실행될 때 왼쪽부터 오른쪽으로 평가되고, 그렇게 한 번 계산된 값이 매 호출에 쓰입니다.

def는 선언이 아니라 실행되는 문장입니다. 모듈을 import하거나 스크립트가 그 줄에 도달하면 파이썬은 함수 객체를 만들고, 그때 = 오른쪽 식을 계산해 결과를 함수 객체에 붙여 둡니다. 호출할 때는 식을 다시 계산하지 않고 붙여 둔 객체를 그대로 매개변수에 연결합니다.

그래서 L=[]의 빈 리스트는 함수당 하나뿐입니다. 인자를 생략한 호출은 모두 이 리스트를 받고, append로 바꾼 내용은 다음 호출에서도 그대로 보입니다.

튜토리얼의 다른 예제도 같은 규칙을 보여 줍니다.

i = 5

def f(arg=i):
    print(arg)

i = 6
f()   # 5

def가 실행된 시점의 i가 5였으므로 기본값은 5입니다. 나중에 i를 6으로 바꿔도 기본값은 그대로입니다.

함수 객체에서 직접 확인하기

계산된 기본값은 함수 객체의 __defaults__ 속성에 튜플로 들어 있습니다. 데이터 모델 문서는 이 속성을 “기본값이 있는 매개변수들의 기본값을 담은 튜플, 기본값이 하나도 없으면 None”으로 설명합니다. 키워드 전용 매개변수의 기본값은 __kwdefaults__ 딕셔너리에 따로 들어갑니다.

위의 f(a, L=[])를 세 번 호출한 뒤 f.__defaults__를 보면 ([1, 2, 3],)이 들어 있을 것입니다. 기본값이 호출 사이에 “기억되는” 곳이 바로 여기입니다. 디버깅하다 함수가 이상한 상태를 가진 것처럼 보이면 이 속성을 찍어 보는 것이 가장 빠른 확인 방법입니다.

어떤 값이 문제가 되나

문제는 기본값을 공유한다는 사실 자체가 아니라, 공유된 객체를 함수가 바꿀 때 생깁니다. 공식 FAQ는 숫자, 문자열, 튜플, None 같은 불변 객체는 바뀔 일이 없으니 안전하고, 딕셔너리·리스트·클래스 인스턴스 같은 가변 객체가 혼란을 부른다고 설명합니다. 튜토리얼도 리스트, 딕셔너리와 함께 “대부분의 클래스 인스턴스”를 꼽습니다.

자주 마주치는 형태는 다음과 같습니다.

  • def add(item, items=[])처럼 받은 컬렉션에 append나 update를 하는 함수
  • def __init__(self, tags=[])에서 self.tags = tags로 저장하는 생성자. 인자 없이 만든 인스턴스들이 모두 같은 리스트를 공유합니다.
  • def log(msg, when=datetime.now())처럼 호출 시각을 기본값으로 쓰려는 함수. 시각은 def가 실행된 순간 한 번만 계산되므로, 이후 모든 호출이 같은 시각을 기록합니다.

마지막 경우는 값이 가변이어서가 아니라 평가 시점 때문에 생기는 문제입니다. 기본값 식은 함수가 정의될 때 한 번만 실행된다는 규칙에서 둘 다 나옵니다.

None으로 받고 함수 안에서 만든다

레퍼런스, 튜토리얼, FAQ가 권하는 방법은 같습니다. 기본값으로 None을 두고, 함수 본문에서 None인지 확인해 새 객체를 만듭니다.

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

이제 []는 함수가 호출될 때마다 실행되므로, 인자를 생략한 호출은 매번 새 리스트를 받습니다. 시각도 마찬가지로 when=None으로 받은 뒤 본문에서 datetime.now()를 부르면 됩니다.

검사는 if L is None으로 씁니다. 짧게 L = L or []로 쓰는 코드도 있는데, 이 식은 None뿐 아니라 거짓으로 평가되는 모든 값을 바꿉니다. 호출하는 쪽이 결과를 받으려고 빈 리스트를 넘겨도 함수는 그 리스트 대신 새 리스트에 값을 넣습니다. 넘긴 리스트는 빈 채로 남습니다.

None 자체가 의미 있는 인자라서 “인자를 생략했다”와 구별해야 한다면, 모듈 수준에 _MISSING = object() 같은 전용 객체를 하나 만들어 기본값으로 쓰고 is로 비교할 수 있습니다. 이 객체는 다른 곳에서 만들어질 수 없으므로 호출자가 실수로 넘길 일이 없습니다.

일부러 공유하는 경우도 있다

FAQ는 이 동작이 쓸모 있을 때도 있다고 덧붙입니다. 계산이 오래 걸리는 함수의 결과를 기억해 두는 메모이제이션 예제입니다.

def expensive(arg1, arg2, *, _cache={}):
    if (arg1, arg2) in _cache:
        return _cache[(arg1, arg2)]
    result = ...  # 오래 걸리는 계산
    _cache[(arg1, arg2)] = result
    return result

* 뒤에 두어 키워드로만 넘길 수 있게 했고, 이름 앞의 밑줄로 내부용임을 드러냈습니다. FAQ는 딕셔너리를 담은 전역 변수를 써도 되며 취향의 문제라고 적습니다. 다만 이 의도를 모르는 사람에게는 앞의 버그와 똑같아 보이므로, 이렇게 쓸 때는 주석을 남겨 두는 편이 좋습니다. 메모이제이션이 목적이라면 표준 라이브러리의 functools.cache(3.9부터)나 functools.lru_cache 데코레이터를 쓰는 방법도 있습니다.

dataclass는 일부를 미리 막아 준다

@dataclass에서 필드 기본값은 생성되는 __init__의 기본 인자가 되므로 같은 문제가 생길 수 있습니다. dataclasses 문서에 따르면 데코레이터는 해시할 수 없는 기본값을 발견하면 ValueError를 냅니다. 해시할 수 없으면 가변일 것이라고 가정하는 방식입니다.

from dataclasses import dataclass, field

@dataclass
class D:
    x: list = []                          # ValueError

@dataclass
class D:
    x: list = field(default_factory=list) # 인스턴스마다 새 리스트

이 검사는 Python 3.11에서 바뀌었습니다. 3.10까지는 list, dict, set 타입만 막았고, 3.11부터는 해시할 수 없는 객체를 모두 막습니다. 문서도 이것이 부분적인 해결책이라고 밝힙니다. 해시 가능한 가변 객체는 통과하므로, 가변 기본값이 필요하면 default_factory에 인자 없는 호출 가능 객체를 넘기는 쪽이 확실합니다.

일반 함수와 클래스에는 이런 검사가 없습니다. def f(L=[])는 아무 경고 없이 정의되므로 코드 리뷰나 린터로 잡아야 합니다.

정리

  • 기본값 식은 def가 실행될 때 한 번 평가되고, 그 객체가 모든 호출에서 재사용됩니다.
  • 함수가 기본값 객체를 바꾸면 그 변경이 다음 호출로 이어집니다. 리스트, 딕셔너리, 대부분의 클래스 인스턴스가 해당합니다.
  • 기본값은 None으로 두고 본문에서 if x is None:으로 새 객체를 만듭니다.
  • dataclass에서는 field(default_factory=...)를 씁니다.

참고 자료

확인일: 2026년 10월 10일 (Python 3.15.0 문서)

블로그로