4

We all know the 'dreaded mutable default argument'

I was surprised to find that there is at least one built-in that uses a mutable default argument, deepcopy (and the related __deepcopy__).

def deepcopy(x, memo=None, _nil=[]):

source

Is this just an overlook by the module's authors or is there something special about deepcopy's behavior that justifies this?

DeepSpace
  • 78,697
  • 11
  • 109
  • 154

1 Answers1

6

Yes, this is intentional, the object is being used as a unique sentinel object. It is never itself mutated, nor are the list object contents ever inspected. It's only purpose is to have a unique object that can be used in an is test.

It is more common to use None as a sentinel to test if something has not been set, but you sometimes need to allow for None to be a valid set value. In the implementation of deepcopy() the _nil object is used to replace None:

y = memo.get(d, _nil)
if y is not _nil:
    return y

This allows memo[d] = None to be set, as memo.get(d) would return None both if memo[d] exists or is missing:

>>> memo = {}; d = 'foo'
>>> _nil = []
>>> memo.get(d) is None
True
>>> memo.get(d, _nil) is _nil
True
>>> memo[d] = None
>>> memo.get(d) is None
True
>>> memo.get(d, _nil) is _nil
False

If you are thinking of using your own unique not-None sentinel object, these days you'd probably want to use object(). But object() didn't exist yet in the Python language when the deepcopy() function was written.

_nil is an argument to make it a local variable. This makes looking up the _nil reference much cheaper than if it were a global in the module, see Why does Python code run faster in a function? This is important in a function that can easily be called a lot when used to copy a deep and wide structure of custom objects. So, as the initial underscore to the name suggests, _nil is an implementation detail.

Also see Is there a way to set a default parameter equal to another parameter value?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343