Published on

Getting Tripped Up By Python Default Function Parameters

Authors

In Python you can define default values for function parameters like so:

def add_time(
        seconds: float = 0,
        minutes: float = 0,
        hours: float = 0,
        days: float = 0,
        weeks: float = 0,
        date: datetime = datetime.datetime.utcnow()
):
    return date + datetime.timedelta(seconds=seconds, minutes=minutes, hours=hours, days=days, weeks=weeks)

These telescoping functions are great as you do not have to define permutations of the above function for each possible combination of parameters.

One thing I found out the hard way today is that while I assumed the values in the above were default values i.e. date will be given the current date each time unless I provide a parameter. This is not the case at least not for complex objects.

In Python default complex arguments are only evaluated once when the def is read by the interpreter. Subsequent function calls will result in the original default object's value mutating instead of being reassigned. This is not the case for simple types like the floats in the above which get reinitialized each time.

To demonstrate consider the following code:

def default_int(blah = 0):
    blah = blah+1
    print(blah)

def default_list(blah = []):
    blah.append("a")
    print(blah)


if __name__ == '__main__':
    print("---------- Default int")
    default_int()
    default_int()
    default_int()
    default_int()
    default_int()

    print("---------- Default list")
    default_list()
    default_list()
    default_list()
    default_list()
    default_list()
    default_list()
    default_list()

Which outputs:

---------- Default int
1
1
1
1
1
---------- Default list
['a']
['a', 'a']
['a', 'a', 'a']
['a', 'a', 'a', 'a']
['a', 'a', 'a', 'a', 'a']
['a', 'a', 'a', 'a', 'a', 'a']
['a', 'a', 'a', 'a', 'a', 'a', 'a']

I am not 100% sure what the internals are for this but when using default parameters it is better to view them as initial values if you are assigning a default to a complex object. It is a default parameter if being assigned to a simple type.

I added a little helper method which safely defaults complex types as recommended here.

def safe_default_parameter(parameter, default):
    if parameter == None:
        return default
    else:
        return parameter

def default_int(blah=0):
    blah = blah + 1
    print(blah)


def default_list(blah=[]):
    blah.append("a")
    print(blah)


def safe_default_list(blah=None):
    blah = safe_default_parameter(blah, [])
    blah.append("a")
    print(blah)


if __name__ == '__main__':
    print("---------- Default int")
    default_int()
    default_int()
    default_int()
    default_int()
    default_int()

    print("---------- Default list")
    default_list()
    default_list()
    default_list()
    default_list()
    default_list()
    default_list()
    default_list()

    print("---------- Safe Default list")
    safe_default_list()
    safe_default_list()
    safe_default_list()
    safe_default_list()
    safe_default_list()
    safe_default_list()
    safe_default_list()

The safe portion outputs what we expect:

---------- Safe Default list
['a']
['a']
['a']
['a']
['a']
['a']
['a']