Correct Style for Python functions that mutate the argument

Question:

I would like to write a Python function that mutates one of the arguments (which is a list, ie, mutable). Something like this:

def change(array):
   array.append(4)

change(array)

I’m more familiar with passing by value than Python’s setup (whatever you decide to call it). So I would usually write such a function like this:

def change(array):
  array.append(4)
  return array

array = change(array)

Here’s my confusion. Since I can just mutate the argument, the second method would seem redundant. But the first one feels wrong. Also, my particular function will have several parameters, only one of which will change. The second method makes it clear what argument is changing (because it is assigned to the variable). The first method gives no indication. Is there a convention? Which is ‘better’? Thank you.

Asked By: Neil Du Toit

||

Answers:

The first way:

def change(array):
   array.append(4)

change(array)

is the most idiomatic way to do it. Generally, in python, we expect a function to either mutate the arguments, or return something1. The reason for this is because if a function doesn’t return anything, then it makes it abundantly clear that the function must have had some side-effect in order to justify it’s existence (e.g. mutating the inputs).

On the flip side, if you do things the second way:

def change(array):
  array.append(4)
  return array

array = change(array)

you’re vulnerable to have hard to track down bugs where a mutable object changes all of a sudden when you didn’t expect it to — “But I thought change made a copy”…

1Technically every function returns something, that _something_ just happens to be None

Answered By: mgilson

The convention in Python is that functions either mutate something, or return something, not both.

If both are useful, you conventionally write two separate functions, with the mutator named for an active verb like change, and the non-mutator named for a participle like changed.

Almost everything in builtins and the stdlib follows this pattern. The list.append method you’re calling returns nothing. Same with list.sort—but sorted leaves its argument alone and instead returns a new sorted copy.

There are a handful of exceptions for some of the special methods (e.g., __iadd__ is supposed to mutate and then return self), and a few cases where there clearly has to be one thing getting mutating and a different thing getting returned (like list.pop), and for libraries that are attempting to use Python as a sort of domain-specific language where being consistent with the target domain’s idioms is more important than being consistent with Python’s idioms (e.g., some SQL query expression libraries). Like all conventions, this one is followed unless there’s a good reason not to.


So, why was Python designed this way?

Well, for one thing, it makes certain errors obvious. If you expected a function to be non-mutating and return a value, it’ll be pretty obvious that you were wrong, because you’ll get an error like AttributeError: 'NoneType' object has no attribute 'foo'.

It also makes conceptual sense: a function that returns nothing must have side-effects, or why would anyone have written it?

But there’s also the fact that each statement in Python mutates exactly one thing—almost always the leftmost object in the statement. In other languages, assignment is an expression, mutating functions return self, and you can chain up a whole bunch of mutations into a single line of code, and that makes it harder to see the state changes at a glance, reason about them in detail, or step through them in a debugger.

Of course all of this is a tradeoff—it makes some code more verbose in Python than it would be in, say, JavaScript—but it’s a tradeoff that’s deeply embedded in Python’s design.

Answered By: abarnert

It hardly ever makes sense to both mutate an argument and return it. Not only might it cause confusion for whoever’s reading the code, but it leaves you susceptible to the mutable default argument problem. If the only way to get the result of the function is through the mutated argument, it won’t make sense to give the argument a default.

There is a third option that you did not show in your question. Rather than mutating the object passed as the argument, make a copy of that argument and return it instead. This makes it a pure function with no side effects.

def change(array):
  array_copy = array[:]
  array_copy.append(4)
  return array_copy

array = change(array)
Answered By: Mark Ransom

From the Python documentation:

Some operations (for example y.append(10) and y.sort()) mutate the
object, whereas superficially similar operations (for example y = y +
[10] and sorted(y)) create a new object. In general in Python (and in
all cases in the standard library) a method that mutates an object
will return None to help avoid getting the two types of operations
confused. So if you mistakenly write y.sort() thinking it will give
you a sorted copy of y, you’ll instead end up with None, which will
likely cause your program to generate an easily diagnosed error.

However, there is one class of operations where the same operation
sometimes has different behaviors with different types: the augmented
assignment operators. For example, += mutates lists but not tuples or
ints (a_list += [1, 2, 3] is equivalent to a_list.extend([1, 2, 3])
and mutates a_list, whereas some_tuple += (1, 2, 3) and some_int += 1
create new objects).

Basically, by convention, a function or method that mutates an object does not return the object itself.

Answered By: Bharel
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.