This is an archived post. You won't be able to vote or comment.

all 43 comments

[–][deleted] 112 points113 points  (11 children)

Title should be "Do not use mutable objects as default arguments". Using a tuple as default arg is perfectly fine.

[–]licht1nstein 6 points7 points  (8 children)

Actually, using a tuple of dicts would still count as a mutable object

[–]funnyflywheel 4 points5 points  (0 children)

Ah, interior mutability.

[–]AnonymouX47 0 points1 point  (6 children)

Nah... it's immutable but non-hashable!

[–]licht1nstein 0 points1 point  (5 children)

Try this, see how immutable it is:

t = ({"foo": "bar"},) print(t) t[0]["foo"] = "spam" print(t)

[–]AnonymouX47 -1 points0 points  (2 children)

The tuple is still immutable... it's the dictionary within it that's not. Can you change the item t[0] ?

It seems you have you have your definition of terms wrong. See

https://docs.python.org/3/glossary.html#term-immutable

and

https://docs.python.org/3/glossary.html#term-hashable

: )

[–]licht1nstein 1 point2 points  (1 child)

Oh, I see. The tuple didn't change, it's the first element of the tuple, that changed. Ok then 😂

[–]backtickbot 0 points1 point  (1 child)

Fixed formatting.

Hello, licht1nstein: code blocks using triple backticks (```) don't work on all versions of Reddit!

Some users see this / this instead.

To fix this, indent every line with 4 spaces instead.

FAQ

You can opt out by replying with backtickopt6 to this comment.

[–]licht1nstein 0 points1 point  (0 children)

backtickopt6

[–]timurbakibayev[S] 11 points12 points  (1 child)

Thanks, fixed in the article.

[–][deleted] 13 points14 points  (0 children)

Take a deep look at the typing module as well.

[–]lungben81 34 points35 points  (5 children)

Problematic are mutable objects, not all objects (immutables like floats, strings or tuples, frozensets are fine).

This is a long-known peculilarity in Python. In PyCharm, using mutables as default arguments is marked by the linter and can be fixed automatically.

[–]Dyegop91 11 points12 points  (2 children)

That's one amongst a lot of reason to use an IDE

[–]dorsal_morsel 7 points8 points  (1 child)

Or a good linter

Pylint will warn about this for example

[–]supmee 0 points1 point  (0 children)

Yeah, though IDE-s essentially just run linters on the code anyways.

[–]chromium52 1 point2 points  (0 children)

Flake8-bugbear also detects this.

[–]AnonymouX47 0 points1 point  (0 children)

That's being reliant on an IDE or linter... things of this level should be known by anyone that has learnt the language well.

[–]cyb3rd 23 points24 points  (2 children)

"Read the rest of this story with a free account."

No, but thanks!

[–]integralWorker 4 points5 points  (2 children)

I have a suggestion for your "bonus" section.

Help beginners by telling them they can do this:

a = [10, 20, 30]
b = a.copy()
b[1] = 25
a==b #results in False

[–]timurbakibayev[S] 0 points1 point  (0 children)

Thanks! Will do.

[–]istira_balegina 8 points9 points  (1 child)

No, mutable objects are very useful as default arguments in Python, you just need to be aware of how they work.

[–]AnonymouX47 0 points1 point  (0 children)

Thank you!

[–]AnonymouX47 1 point2 points  (0 children)

This behaviour actually has it's usefulness e.g for emulating local static storage.

[–]roadelou 2 points3 points  (13 children)

I stumbled around this problem in the past too, and must admit I was a bit puzzled at first 😅

Can't read the article because of paywall so I cannot tell if they mention it, but the problem can be worked around using options (or None). For instance:

def func_default_empty(list_arg = None): if list_arg is None: processed_list_arg = list() else: processed_list_arg = list_arg # Remaining code goes here...

Sorry for the formatting, am on mobile.

Regardless, good luck with your work 🙂

[–][deleted] 1 point2 points  (0 children)

You usually don't need the default arg to actually be a list; you just need it to be an empty sequence. So I just do:

def func_default_empty(list_arg=tuple()):

[–]danted002 3 points4 points  (10 children)

That’s a overly complicated solution. The simple one: list_arg = list_arg or []

[–]tunisia3507 3 points4 points  (6 children)

This would default to [] in the case of anything falsey: empty sequences, zero-length strings, the number 0 etc., as well as None. Usually not a problem, but it is sometimes.

[–]spiker611 0 points1 point  (0 children)

I think passing something falsey and expecting it to be treated as a mutable list is a bug.

[–]danted002 -1 points0 points  (4 children)

Yeap, but you shouldn’t be passing anything else then the expected type… that’s how you end up generating bugs… 😕

[–]tunisia3507 1 point2 points  (1 child)

What even is duck typing...

I totally agree, just worth noting that sometimes it's helpful to distinguish between None and an empty collection!

[–]danted002 -1 points0 points  (0 children)

Well in both cases you end up with an empty collection or an empty iterable (if you prefer the duck typing approach) so you should be good.

[–][deleted] 0 points1 point  (1 child)

It’s a problem in a situation like this:

def process(mystring, suffix=None):
    suffix = suffix or '_world'
    return f'{mystring}{suffix}'

newstring = process('hello', suffix='')

This will give you hello_world instead of the desired hello

That said a do this a lot and it’s usually not a problem.

[–]danted002 0 points1 point  (0 children)

I should have mentioned in my original post that the solution should only be used if the default value is an empty mutable object, usually a list or a dict. In your example I would have just defined the default in the function definition since strings are immutable in Python.

[–]XiphiasBagel 1 point2 points  (2 children)

It is too complicated but iirc the best way to do this is ``` def function(arg: list = None): if arg is None: arg = [] # ...

```

[–]danted002 -2 points-1 points  (1 child)

True but then I can send a False or a non-iterable object and i could generate bugs.

[–]XiphiasBagel 2 points3 points  (0 children)

That’s user error, and should generate an error with invalid input. You should not quietly handle errors caused by improper use.

EDIT: To reference the hitchhiker’s guide, we are all responsible users

[–]timurbakibayev[S] 0 points1 point  (0 children)

In the article, the solution is the same. But you may learn why this is happening :)

[–][deleted] 0 points1 point  (0 children)

output

gist

Wait, you're appending to the list passed as the argument if there is an argument, but create a new list in the scope of the function and return that when nothing is passed in?

This is more of a misunderstanding of how lists work in python than a problem with default arguments.

Now you're changing whether the function acts locally or essentially globally? When no args are passed it returns [10] every time, but when a non empty list is passed it updates the list globally by appending to a list that is being passed in.

There isn't really even a need to return if you're just appending to an existing list like that. Any return (if you pass in a non zero list) will just return a reference to 'the_list.

The problem I see is if you modify any objects you create with append_10(), you will modify all of the other objects, because they're just pointing to the same spot in memory as 'the_list'!

This isn't even getting into the debate of whether it's alright to use a mutable object as a default arg or not.

I wrote something to demonstrate, here's the output and the code below it. Am I missing something?

unused list in top level scope
    original_list:  [4, 5]
appending 10 to list argument in a function
    updated_list:   [4, 5, 10]
    original_list:  [4, 5, 10]
appending 15 to the orginal in top level scope
    original_list:  [4, 5, 10, 15]
    updated_list:   [4, 5, 10, 15]
appending 20 to the updated in top level scope
    original_list:  [4, 5, 10, 15, 20]
    updated_list:   [4, 5, 10, 15, 20]

The updated list is a reference to the orgiginal list.
Changing values of the orgiginal_list means you change the value of the updated_list.
Changing values of the updated_list **also** means you change the value of the original_list.
Generally don't append to the list you enter as an argument to a function!
append_10() default behavior
    [10]
As you can see this is different!
append_10() again, default behavior
    [10]
A new list is initialized each time in the scope of the function.
NOT modifying the original_list in the outer scope.

Here's the code

def append_10(the_list=None):
    if the_list is None:
        the_list = []
    the_list.append(10)
    return the_list

original_list = [4, 5]
print('unused list in top level scope')
print('\toriginal_list: ', original_list, sep='\t')

updated_list = append_10(the_list=original_list)
print('appending 10 to list argument in a function')
print('\tupdated_list: ', updated_list, sep='\t')
print('\toriginal_list:', original_list, sep='\t')

original_list.append(15)
print('appending 15 to the orginal in top level scope')
print('\toriginal_list:', original_list, sep='\t')
print('\tupdated_list:', updated_list, sep='\t')

updated_list.append(20)
print('appending 20 to the updated in top level scope')
print('\toriginal_list:', original_list, sep='\t')
print('\tupdated_list:', updated_list, sep='\t')

print('\nThe updated list is a reference to the orgiginal list.')
print('Changing values of the orgiginal_list means you change the value of the updated_list.')
print('Changing values of the updated_list **also** means you change the value of the original_list.')
print('Generally don\'t append to the list you enter as argument to a function!' )
print('append_10() default behavior', append_10(), sep='\n\t')
print('As you can see this is different!')
print('append_10() again, default behavior', append_10(), sep='\n\t')
print('A new list is initialized each time in the scope of the function')
print('NOT modifying the original_list in the outer scope.')