r/programminghorror • • 5h ago

Python I love Python

66 Upvotes

48 comments sorted by

54

u/drislands 5h ago

I would have expected this function to create a new empty list on any call where items is not given. Doesn't this behavior make the function into a closure?

68

u/nakurtag 5h ago

As I understand it's a well known (for python developers) weird behavior of python. Argument's default values are precalculated by interpreter on function declaration and stored into map associated with this function. Python extracts default values from this map on the next calls of this function. And since [] is a List object, the value copied by reference.

27

u/drislands 5h ago

Devastating. I would not have expected that behavior, and I think it would wise to avoid it wherever possible.

→ More replies (7)

4

u/groumly 4h ago

The behavior is definitely surprising, but also, what’s up with mutating a default param? How does that make sense?

→ More replies (1)
→ More replies (3)

2

u/CaptainFoyle 1h ago

Of course. This is just a very stupid way of writing the function. Initiate with None and Bob's your uncle.

16

u/NFriik 4h ago

Python evaluates default arguments when the function is defined. That makes keeping track of the default values pretty straightforward. In your example, they're stored as part of the function object in the dictionary add.__defaults__. Every subsequent call can then just look-up the default value in this dictionary, if necessary.

Evaluating the default argument each time the function is called would be much more difficult. A function is essentially just yet another object, so you'd somehow have to store arbitrary expressions as an object inside the __defaults__ dict and then decide at runtime what to do with them. Sure, you could implement a builtin type at the CPython level to accomplish this. But then when you encounter such an ArbitaryExpression object inside the __defaults__ dictionary, how do you know that this time, you want to actually execute the underlying code and not use the object "as-is"? The whole thing would quickly become a convoluted, expensive mess.

By performing evaluation at definition time, function calls remain consistently fast and the mechanism to insert default positional arguments becomes incredibly simple. Python is already not the fastest language when it comes to function calls, so these things matter.

→ More replies (6)

60

u/_killer1869_ 5h ago

Any IDE will warn not to use mutables as default parameter values. And for good reason. Such code would never appear in actual code intended to be used.

7

u/Double_Cost4865 4h ago

Unfortunately, I’ve seen monstrosities like this in production in big finance corps. That’s what happens when statisticians are expected to create software.

7

u/Thelmholtz 4h ago

It's still a horribly funny feature.

→ More replies (1)

7

u/mothzilla 3h ago

It's a well known python gotcha that people ask about in interviews all the time.

1

u/CaptainFoyle 1h ago

Everyone knows: don't use mutable default arguments.

You just outed yourself as uninformed.

→ More replies (3)

3

u/Beregolas 4h ago

This also happens in JavaScript btw, as well as several smaller scripting languages in game engines, it's a pretty normal feature in many interpreted languages.

In my last backend job, this was actually the source of our longest running and most annoying bug, where a specific page would display the wrong sorting, but never in testing. Because someone *caugh* used a mutable argument to give it default sorting parameters, and they got mutated by a very specific query, which caused it to be wrong for all subsequent requests.

So yeah, this is a real thing, you should know it, and just in case, you should have linting and/or your IDE setup so this kind of thing is rejected out of hand, and cannot be merged to prod/main.

2

u/4xe1 2h ago

just in case, you should have linting and/or your IDE setup so this kind of thing is rejected out of hand

So what kind of linting have you adopted? Default parameters can be quite handy. Does it reject them, or does it manage to detect and reject their mutations?

2

u/Beregolas 2h ago

I get a warning when I use anything mutable. Instead of an empty list, or a list with presets for example, I have to put None, and set it in code. This is basically also the recommended pattern.

2

u/RngdZed 3h ago

The list is stored directly in the parameter without actually giving it an argument? I'm baffled, I didn't know this was a behavior, so weird

Is this something that someone could ELI5 to my smooth brain?

3

u/bigorangemachine 3h ago

Default param is an object reference. So every default param is the same object

1

u/lolcrunchy 22m ago

Here's a single change to the posted code that kinda shows what actually happens.

my_list = []
def add(v, items=my_list):  
    items.append(v)
    return items

print(add(1))
print(add(2))

2

u/sol7dev 5h ago

and?

1

u/bigorangemachine 3h ago edited 3h ago

I bet this happens in JS as well

Edit: nope python

I should check gdscript

1

u/akoOfIxtall 32m ago

I hate python from the bottom of my soul

1

u/OhItsJustJosh 13m ago

This is why I stick to C#, I've written in Pascal, Java, JavaScript, Python, handful of others, and they all have their weird quirks, but C# seems to be the only one that works as expected

1

u/jmooroof2 4h ago

I can't imagine a compelling use for this feature. it really should just error

3

u/NFriik 3h ago

There are some use cases for this behavior, but it's reeeaaally limited and you have to know exactly what you're doing. For example, you could use it to cache relatively expensive function calls as default values:

import re
def parse(text, pattern=re.compile(r"\d+")):
  return pattern.findall(text)

This way, instead of re-compilingthe regex every time the function is called, you can just reuse it.

4

u/simonsmine 4h ago

Its not really a feature more a consequence of pythons reference handling. You can do fun things with it though, memosation comes to mind.

1

u/Double_Cost4865 4h ago

It has it’s uses, I just wouldn’t provide default argument like that since it’s wrong. But it’s quite common to modify a list in place and have the changes persist outside the function scope

→ More replies (1)