Pythonic way to write a function which modifies a list?

Viewed 479

In python function arguments are passed by object reference. This means the simplest code to modify a list will modify the object itself.

a = [1,2,3]

def remove_one(b):
    b.remove(1)

remove_one(a)
print(a)

The method remove_one returns nothing. If that was a public method one has to assume it will modify the object and isn't thread safe inside the method.


A second approach would be to structure the code snippet like this:

a = [1,2,3]

def remove_one(b):
    b.remove(1)
    return(b)

print(remove_one(a[:]))

Here the content of a wasn't modified and a new list is returned. This puts a lot of responsibility on the method caller.


Then again list comprehensions, the pythonic approach of modifying list contents always create a new object.

a = [1,2,3]

def remove_one(b):
    b = [num for num in b if b!=1]
    return(b)

print(remove_one(a))

It isn't clear to me if there is a "pythonic" way to do it so I'll make some assumptions to be challenged:

  • It's safer to assume that a function taking a mutable object will modify it's content
  • There is a reason why list comprehensions don't also directly modify the object. I just don't know it
  • The "pythonic" way to write functions that use list comprehensions to modify the mutable argument isn't clear
2 Answers

It looks to me your not really looking for a simple element delete but rather a more complex mutation the list can have. If you are, however, a simple call to .delete() is what you're looking for.

On more complex mutations, one good example case study is the heapq module from the standard lib. And they do it pretty poorly IMHO. Lets see.

import heapq

q = [5, 6, 3]

heapq.heapify(q)      # this mutates q
a = heapq.heappop(q)  # this aswell

If you must know, the heap queue algorithm keeps the lowest item first in the list. So q is now [5, 6] and a is 3.

This is similar to what you have, and it is in the standard lib. Whatever you make of that.

However, I strongly disagree with this convention. All calls to the heapq module functions will require passing that q as the first argument, and sometimes q is mutated . Yet q itself is never returned.

This is the exact problem a class solves. So why avoid a class?

import heapq

class HeapQ(list):
    def __init__(self, a_list):
        super().__init__(a_list)  # calling the base `list()`
        heapq.heapify(self)

    def pop(self):
        return heapq.heappop(self)

    def push(self, value):
        heapq.heappush(self, value)

And the calling convention is a bit more logical.

>>> q = HeapQ([5, 6, 3])
>>> a = q.pop()
>>> q
[5, 6]
>>> a
3

This is way more pythonic, and with less surprises. Mutations on q are not surprising, every mutation is a call on q. q.pop(), q.push(0), both mutate q.

I actually do a more elaborate version of this all the time I need the heapq module.

TLDR: If you modify an argument, do not return it.


list.sort() sorts the list in place. In order to remind you of that fact, it does not return the sorted list.

Design and History FAQ

It is idiomatic to indicate whether you modify an argument by not returning it. For example, your first case is correct, your second is not:

def remove_one(b):
    b.remove(1)  # modify, do not return

If you do not modify an argument but create a copy, you must return the copy for the operation to be meaningful:

def one_removed(b):
    c = b.copy()  # create new object...
    c.remove(1)
    return c      # ...and return it

It is common for function/method names to reflect whether they actively modify their argument. Modifying functions tend to use active verbs, whereas non-modifying functions tend to use passive verbs or adjectives.


There is no "pythonic" choice whether to modify the argument or not - operating on the original or a copy are two fundamentally different operations. Both are valid depending on the use-case.

>>> items = [4, 3, 10, 2, 5]
>>> sorted(items)
[2, 3, 4, 5, 10]
>>> items.sort()
>>> items
[2, 3, 4, 5, 10]

In general, mutating an argument is faster in Python, whereas creating a new result is easier to reason about. In specific, comprehensions represent functional programming operations (namely map and filter) -- they are designed not to modify their iterable.


Whether to return an argument due to modification only concerns said argument. For example, list.pop modifies the list and does not return it -- however, it does return the popped element.

Related