33

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.

mgilson
  • 300,191
  • 65
  • 633
  • 696
Neil Du Toit
  • 441
  • 1
  • 4
  • 4
  • 2
    Why does the first one "feel wrong"? – BrenBarn Sep 24 '14 at 22:39
  • I suppose I'm just used to the second. As I say also, when there are multiple parameters, it is less clear what's going on with the first. – Neil Du Toit Sep 24 '14 at 22:42
  • 3
    Notice that `list.append` itself returns nothing, because it mutates the value. That's true of almost everything in the builtins and the stdlib. – abarnert Sep 24 '14 at 22:43
  • 1
    There are a few notable exceptions, but only when getting access to the desired return value would be cumbersome/messy otherwise. e.g. `list.pop` mutates the list and also returns the popped value. I'm sure that @abarnert would agree that these cases are the exception, not the rule. – mgilson Sep 24 '14 at 22:47
  • Slightly different. The value list.append changes is the object, not the argument (4 stays 4). But point taken. Thanks for the help. – Neil Du Toit Sep 24 '14 at 22:52
  • 1
    @NeilDuToit: The object _is_ an argument. (Python even makes you declare the corresponding parameter explicitly, as `self`, in the implementation.) – abarnert Sep 24 '14 at 22:56

4 Answers4

36

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 ...

mgilson
  • 300,191
  • 65
  • 633
  • 696
  • 1
    +1. But also because this means that each statement mutates exactly one thing once, which isn't true in languages where you can chain together mutating function calls, assignments, etc., and it's almost always the leftmost thing in the statement that gets mutated. – abarnert Sep 24 '14 at 22:44
  • 1
    It also might be worth mentioning the convention that a function that returns a changed copy, without mutating anything, would be named `changed`. – abarnert Sep 24 '14 at 22:46
  • 2
    @abarnert -- I'm not following that one. Obviously `changed` isn't a good name for anything -- a function's name should say something more about what it *does* than just "this function changes the foo"... – mgilson Sep 24 '14 at 22:50
  • 4
    Well, `change` doesn't mean anything in the first place… But `changed` is the past participle of `change`, so it does whatever `change` would do, except by making and returning a copy instead of mutating in-place. For a more useful/realistic example, `sort` vs. `sorted`. – abarnert Sep 24 '14 at 22:52
  • 2
    I'll write my own answer to explain; there's more to be said here, but this answer is good on its own, and should be accepted. – abarnert Sep 24 '14 at 22:55
30

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.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • 2
    note `list.pop()` _could_ return a (new_list, element) pair – joel Jun 24 '19 at 21:45
  • I'd also love to hear why `__iadd__` is an exception – joel Jun 24 '19 at 21:50
  • @joel `__iadd__` is simply one of many expression operators that are specified as modifying their argument: see [In-place Operators](https://docs.python.org/3/library/operator.html#in-place-operators). Because each function has an operator syntax such as `+=` they need to return a value too because expressions always have a value. – Mark Ransom Dec 23 '21 at 16:40
6

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)
Community
  • 1
  • 1
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • I would call the function `changed` rather than `change`. This is somewhat of a convention that a pure function that returns something is named by a noun, such as `sum` or `product` or `sorted` (in the case of `sorted`, it's a [nominalised past participle](https://en.wikipedia.org/wiki/Nominalization) rather than an actual noun); whereas a function performing an action and returning `None` is named by a verb, such as `sort` or `append`. There are exceptions of course, but the convention helps understand the distinction between similar functions, such as `sort`/`sorted` or `change`/`changed`. – Stef Oct 29 '21 at 13:25
  • 1
    @Stef I can't argue, but my convention is to change as little from the question as I can. I'll leave you with this closing thought: There are 2 hard problems in computer science: cache invalidation, naming things, and off-by-1 errors. – Mark Ransom Oct 30 '21 at 00:12
1

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.

Bharel
  • 23,672
  • 5
  • 40
  • 80