Почему изменяемые типы не рекомендуется использовать в качестве значений аргументов по умолчанию в Python? Если вы забрались по карьерной лестнице выше джуниора, то наверняка задумывались. И наверняка ответ был таким: «Это приводит к странным побочным эффектам». Но я почти наверняка уверен, что только малая часть разработчиков на Python сделала шаг дальше и разобралась, почему такое поведение у языка.

Давайте разберёмся сначала с тем, с какими побочными эффектами имеем дело. Напишем вот такую функцию:

def foo(param=[]):
    print(param)

>>> foo()
[]

Сколько её ни вызывай, значение, выводимое на экран, не изменится. Никаких побочных эффектов. Но если её модифицировать вот так:

def foo(param=[]):
    param.append(1)
    print(param)

То обнаружим, что значение на экране будет меняться в зависимости от того, сколько раз вызвана функция:

>>> foo()
[1]
>>> foo()
[1, 1]
>>> foo()
[1, 1, 1]

Но мы ведь хотели не этого. Такое поведение функций — один из лидеров разного рода списков «Х самых странных вещей в Python». Хотя, если разобраться с внутренней «кухней» языка, то всё встанет на свои места и «странность» превратится в логичное поведение.

Почему же так получилось? Всё из‑за того, что функция — это объект, как и все другие сущности в Python. При создании объекта дефолтные значения параметров упаковываются в кортеж foo.func_defaults.

Списки в Python — тип изменяемый. А объект функции создаётся один раз. Так что объект, на который ссылается foo.func_defaults[0], и param внутри функции — один и тот же:

def foo(param=[]):
    print(id(param))

>>> foo()
4494207600
>>> id(foo.func_defaults[0])
4494207600

Правильным будет передача None в качестве значения по умолчанию. А в теле функции уже инициализировать изменяемый объект. А подсказку по типу параметра прописывать в докстринге или, если используется Python 3.5+, то использовать PEP 0484:

def foo(param=None):
    """
    :type param: list
    """
    param = [] if param is None else param
    param.append(1)
    print(param)

>>> foo()
[1]
>>> foo()
[1]

Для коротких функций в 5–6 строк добавление проверки на None может показаться «замусориванием» кода. В этих случаях нужно хорошо подумать: оставить эту проверку и обезопасить код или убрать её. Я за безопасность: потом, делая рефакторинг, можно пропустить изменения, которые внесут побочные эффекты.