26

If you go to https://david.goodger.org/projects/pycon/2007/idiomatic/handout.html#default-parameter-values, you can find the following:

def bad_append(new_item, a_list=[]):
    a_list.append(new_item)
    return a_list

def good_append(new_item, a_list=None):
    if a_list is None:
        a_list = []
    a_list.append(new_item)
    return a_list

There's even an "Important warning" on python.org with this very same example, though not really saying it's "better".

Why is the "good" syntax over a known issue ugly like that in a programming language that promotes "elegant syntax" and "easy-to-use"? Or to put it another way, why is there no simpler alternative built into the language?

I think a better way would probably being able to do something in the def itself, in which the name argument would be attached to a "local", or "new" within the def, mutable object. Something like:

def better_append(new_item, a_list=immutable([])):
    a_list.append(new_item)
    return a_list

Why hasn't something like this been done?

29
  • 3
    bad_append and good_append are neither good nor bad. They both have their uses.
    – unutbu
    Commented Apr 14, 2010 at 18:24
  • 3
    honestly, I've yet to see something like bad_append which wasn't bad. If it wasn't actively buggy, it was just syntax abuse (trying to get out of defining an object). Sometimes it's reasonable in inner functions -- but even there, very rarely.
    – moshez
    Commented Apr 14, 2010 at 18:33
  • 1
    See stackoverflow.com/questions/1132941/… Commented Apr 14, 2010 at 18:42
  • 5
    Let me be blunt: your example is flawed. Either you want to modify the passed list, or you want to generate a new one, but your example tries to do both at the same time. The ugliness is a direct result of not acknowledging the flaw. As my answer states, you either want to remove the default argument, or you want to make a copy of the input list - and Python philosophy prefers that the copy be explicit. I'd welcome a different example that states the problem in a more realistic light. Commented Apr 16, 2010 at 4:02
  • 1
    @Cawas, There isn't a the expected behaviour; there's only your expected behaviour. I expect the actual behaviour, personally. It seems horrendously obvious to me (because I am very used to Python's semantics). When I was new to Python, like most people, my expectations probably would have varied from case to case. A lot of people find the non-delayed expression evaluation in the case of a function call or an arithmetic expression, even if they don't in all cases. There are cases where an expression should be evaluated once and cases where it should be evaluated every time, and people Commented Apr 16, 2010 at 18:21

8 Answers 8

11

This is called the 'mutable defaults trap'. See: http://www.ferg.org/projects/python_gotchas.html#contents_item_6

Basically, a_list is initialized when the program is first interpreted, not each time you call the function (as you might expect from other languages). So you're not getting a new list each time you call the function, but you're reusing the same one.

I guess the answer to the question is that if you want to append something to a list, just do it, don't create a function to do it.

This:

>>> my_list = []
>>> my_list.append(1)

Is clearer and easier to read than:

>>> my_list = my_append(1)

In the practical case, if you needed this sort of behavior, you would probably create your own class which has methods to manage it's internal list.

3
  • Sorry @Cawas, I am posting stream-of-consciousness style this morning. See edits.
    – Seth
    Commented Apr 14, 2010 at 18:31
  • Another thing to take away is that it is generally a good idea not to ever default parameters to empty mutable objects like [] or {}. Commented Apr 14, 2010 at 18:33
  • 1
    your edit came 7 seconds after my comment, so I deleted it ;) While I do want to figure my own way to write that to look better, the question here is "why Python is like that" and not "how can I fix it". I'm just trying to understand better the concepts behind it.
    – cregox
    Commented Apr 14, 2010 at 18:33
6

Default arguments are evaluated at the time the def statement is executed, which is the probably the most reasonable approach: it is often what is wanted. If it wasn't the case, it could cause confusing results when the environment changes a little.

Differentiating with a magic local method or something like that is far from ideal. Python tries to make things pretty plain and there is no obvious, clear replacement for the current boilerplate that doesn't resort to messing with the rather consistent semantics Python currently has.

12
  • Now, this is more the kind of answer I was expecting. But why do you say Python semantics is so consistent? I tend to think we always have room for improvement. What about having a different statement from def that isn't evaluated at the time of execution, to make them be evaluated after any kind of mutable or immutable object?
    – cregox
    Commented Apr 16, 2010 at 16:08
  • 1
    I didn't say there wasn't room for improvement; I think there are many things about Python that are suboptimal. I fully admit this is a nasty little thing you have to deal with. However, in the tough decision to be made here, I don't think I've seen a solution that works better without a significant change to how Python works. Commented Apr 16, 2010 at 17:44
  • The other basic option is to, instead of passing an object as a default argument, to pass an expression that is evaluated each time the function is called. This would introduce a new thing to Python that defers execution (heretoforth functions are the only real thing that does it) and potentially change the algorithmic complexity of some operations from O(1) to O(n) (if an expensive operation is conducted time after time). It would require a new pattern foo = bar(); def baz(qux=foo) to do the opposite of def(qux=sentinel): if qux is sentinel: qux = bar() when it matters for your case. Commented Apr 16, 2010 at 17:49
  • The fact that this pattern would probably come up less often is somewhat encouraging, but would have the negative effect of less awareness. (It's also far too late in the game to change this outright for a widely-used language like Python.) Commented Apr 16, 2010 at 17:50
  • 2
    @cregox Please avoid swearing on Stack Overflow.
    – Kyll
    Commented Jun 16, 2016 at 8:23
5

The extremely specific use case of a function that lets you optionally pass a list to modify, but generates a new list unless you specifically do pass one in, is definitely not worth a special-case syntax. Seriously, if you're making a number of calls to this function, why ever would you want to special-case the first call in the series (by passing only one argument) to distinguish it from every other one (which will need two arguments to be able to keep enriching an existing list)?! E.g., consider something like (assuming of course that betterappend did something useful, because in the current example it would be crazy to call it in lieu of a direct .append!-):

def thecaller(n):
  if fee(0):
    newlist = betterappend(foo())
  else:
    newlist = betterappend(fie())
  for x in range(1, n):
    if fee(x):
      betterappend(foo(), newlist)
    else:
      betterappend(fie(), newlist)

this is simply insane, and should obviously be, instead,

def thecaller(n):
  newlist = []
  for x in range(n):
    if fee(x):
      betterappend(foo(), newlist)
    else:
      betterappend(fie(), newlist)

always using two arguments, avoiding repetition, and building much simpler logic.

Introducing special-case syntax encourages and supports the special-cased use case, and there's really not much sense in encouraging and supporting this extremely peculiar one -- the existing, perfectly regular syntax is just fine for the use case's extremely rare good uses;-).

2
  • Alex, the first code don't need the if as well. Just write samething as second, but replace betterappend(foo(), newlist) to newlist = betterappend(foo()). But thanks for pointing this is potentially a "extremely specific" use case... Maybe it's just a matter of re-thinking on how to code, like the "variable vs name" little milestone.
    – cregox
    Commented Apr 14, 2010 at 18:45
  • +1, you're right. People don't usually design a method that accepts an argument and returns that same argument - even with the intention of making that argument optional. Know exception to this rule: memcpy. Commented Jan 20, 2011 at 16:17
3

I've edited this answer to include thoughts from the many comments posted in the question.

The example you give is flawed. It modifies the list that you pass it as a side effect. If that's how you intended the function to work, it wouldn't make sense to have a default argument. Nor would it make sense to return the updated list. Without a default argument, the problem goes away.

If the intent was to return a new list, you need to make a copy of the input list. Python prefers that things be explicit, so it's up to you to make the copy.

def better_append(new_item, a_list=[]): 
    new_list = list(a_list)
    new_list.append(new_item) 
    return new_list 

For something a little different, you can make a generator that can take a list or a generator as input:

def generator_append(new_item, a_list=[]):
    for x in a_list:
        yield x
    yield new_item

I think you're under the misconception that Python treats mutable and immutable default arguments differently; that's simply not true. Rather, the immutability of the argument makes you change your code in a subtle way to do the right thing automatically. Take your example and make it apply to a string rather than a list:

def string_append(new_item, a_string=''):
    a_string = a_string + new_item
    return a_string

This code doesn't change the passed string - it can't, because strings are immutable. It creates a new string, and assigns a_string to that new string. The default argument can be used over and over again because it doesn't change, you made a copy of it at the start.

3
  • Those are 3 very good points here! 1. Is this better than using the None workaround? 2. I totally agree with not having a default argument if the intent is really to modify the passed list, but isn't that just one more reason why it's a flawed behavior? 3. I really know nothing about generators yet (off to research for a little while).
    – cregox
    Commented Apr 14, 2010 at 22:51
  • This is a very nice explanation of generators: stackoverflow.com/questions/231767/…
    – cregox
    Commented Apr 14, 2010 at 23:01
  • 1
    Well stated. The concept of "immutable" isn't reified in the Python implementation; it's merely a categorization that we apply after the fact, based on the interfaces of types. The problems with a list as a default argument don't occur because the list is mutable; they occur because it is mutated. Using an immutable type merely denies you the ability to err in that way. Commented Sep 19, 2022 at 0:02
2

What if you were not talking about lists, but about AwesomeSets, a class you just defined? Would you want to define ".local" in every class?

class Foo(object):
    def get(self):
        return Foo()
    local = property(get)

could possibly work, but would get old really quick, really soon. Pretty soon, the "if a is None: a = CorrectObject()" pattern becomes second nature, and you won't find it ugly -- you'll find it illuminating.

The problem is not one of syntax, but one of semantics -- the values of default parameters are evaluated at function definition time, not at function execution time.

2
  • Sooo, what you're saying we eventually just get so used with the ugly syntax that we can't see it as ugly anymore? As I said there, ".local" might not be the better option, I don't know, but I think something should go right there in the argument definition, as this is a definition of the "function". Plus it would look much better.
    – cregox
    Commented Apr 14, 2010 at 19:00
  • Note that this property does not do what you want because it is still evaluated exactly once. The magic local attribute would have to be syntax, not a property. Commented Apr 16, 2010 at 18:09
1

Probably you should not define these two functions as good and bad. You can use the first one with list or dictionaries to implement in place modifications of the corresponding objects. This method can give you headaches if you do not know how mutable objects work but given you known what you are doing it is OK in my opinion.

So you have two different methods to pass parameters providing different behaviors. And this is good, I would not change it.

2
  • I did not gave the names, they come from python.net
    – cregox
    Commented Apr 14, 2010 at 18:37
  • @Cawas, I know and I sympathize with your question. My answer was that you should not...
    – joaquin
    Commented Apr 14, 2010 at 18:47
0

I think you're confusing elegant syntax with syntactic sugar. The python syntax communicates both approaches clearly, it just happens that the correct approach appears less elegant (in terms of lines of syntax) than the incorrect approach. But since the incorrect approach, is well...incorrect, it's elegance is irrelevant. As to why something like you demonstrate in better_append is not implemented, I would guess that There should be one-- and preferably only one --obvious way to do it. trumps minor gains in elegance.

5
  • 2
    Well, the good_append doesn't seem anything near obvious to me.
    – cregox
    Commented Apr 14, 2010 at 18:58
  • 2
    I don't know. It's obvious to me that the good_append would work. It was not obvious to me that the bad_append wouldn't work. Perhaps a corollary of one obvious right way to do a given thing is that there are an arbitrary number of non-obvious wrong ways to do it.
    – cmsjr
    Commented Apr 14, 2010 at 19:05
  • 1
    precisely: it's not obvious why bad_append wouldn't work. That's what makes good_append not obvious to come up with, thus not an "obvious way to do it".
    – cregox
    Commented Apr 14, 2010 at 19:28
  • That presupposes that bad_append is the more obvious approach, I'm not sure it is.Even if it is one of the less obvious features of language that strives for the obvious, it still seems more obvious than a specialized syntax to denote non-standard usage of parameters.
    – cmsjr
    Commented Apr 14, 2010 at 20:25
  • 1
    Given the syntax for immutable names, it is the more obvious approach for mutable ones. And without knowing is unlikely to use optional arguments, which is essentially what default argument does. I'm not suggesting a specialized syntax, I'm suggesting a generic syntax. Anyway, nobody here answered the real question yet: why it stays ugly like that? People are basically suggesting it's because "nobody came up with a better idea" from my point of view, since there's not a single quote from Python creators about it, just guesses of experienced users.
    – cregox
    Commented Apr 14, 2010 at 21:21
0

This is better than good_append(), IMO:

def ok_append(new_item, a_list=None):
    return a_list.append(new_item) if a_list else [ new_item ]

You could also be extra careful and check that a_list was a list...

2
  • 1
    This behaves strangely when called like some_list = []; ok_append(4, some_list) in that it does not append to my list but makes a new one. This is why I always check if foo is not None and not if foo when I really mean the former. This is also odd in that it always returns None or a single-item list since list.append mutates the list and returns None. Commented Apr 16, 2010 at 18:08
  • 2
    Typechecking for list wouldn't be "extra careful", it would simply be awful practice in Python. Commented Apr 16, 2010 at 18:08

Not the answer you're looking for? Browse other questions tagged or ask your own question.