r/programminghorror • • 20h ago

Python I love Python

155 Upvotes

65 comments sorted by

94

u/drislands 19h 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?

112

u/nakurtag 19h 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.

2

u/trutheality 15h ago

The behavior isn't even that weird: if you were to create some mutable global and set it as a default parameter in a function you wouldn't be surprised that it's accumulating changes from different calls to the function.

The key part is recognizing that the LHS in a default parameter assignment runs when the function is defined and not when it is called.

6

u/nakurtag 15h ago

Name one language with the same behavior

0

u/Double_Cost4865 14h ago

I mean, C/C++? You can pass pointers/references and they will modify in place

55

u/drislands 19h ago

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

49

u/Xelanybor 19h ago

yeah, usually the workaround is to have items = None as the default, and then some kind of check at the top of the function like items = items if items else []

looking at it now I see why it's weird lol but you definitely get used to it

18

u/ZylonBane 17h ago

Which defeats the entire purpose of the default value feature.

13

u/notjfd 14h ago

The default value is fine as long as it is not a mutable object. So things like integers and strings are completely fine to use as default values.

-4

u/ZylonBane 14h ago

Yes, we're all aware. Nobody has disputed that.

3

u/shunabuna 15h ago

its probably for optimisation to reduce the amount of instructions needed to initiate a function call. On the plus side, its an easy place to define a cache

8

u/AdreKiseque 17h ago

What the fuck

5

u/Sulungskwa 17h ago

There's definitely popular lint rules telling you you should never ever ever do this

2

u/bigorangemachine 10h ago

This is why you gotta use a linter.

Stupid small stuff like this.

Also, I found out later the reason I hated ruby was because of robocop... not ruby

7

u/groumly 18h ago

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

5

u/ChemTechGuy 15h ago

The default parameter is just a reference to the list. So technically the parameter isn't being changed at all. One of the oddities of pass-by-reference

2

u/groumly 9h ago

That’s not what I meant.

Mutating input is already not a good pattern, but mutating a default param is even worse.

You can’t tell if it was passed or not, so even without the interning oddity, you end up mutating something that nobody but you has access to.

1

u/ChemTechGuy 9h ago

Go complain to Guido van Rossum then, I'm just telling you how it works

3

u/CaptainFoyle 15h ago

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

22

u/NFriik 18h 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.

6

u/4xe1 17h ago edited 16h ago • Mindblown

Default data defined at the function level is perfectly normal, in many languages string literals live in the code too, they don't get copied every time they are used. What's more disturbing to me is that the default data is mutable.

1

u/gundam1945 4h ago

The explanation makes sense but the behavior is probably not expected. Though if I need to add to a list I usually make the variable outside first and extend in the function.

Never encounter such a use case in this example.

-4

u/ZylonBane 17h ago

You make it sound like it would be absurdly complex or slow to just clone any non-primitive default value.

13

u/4xe1 17h ago

Because it is. Shallow copy versus deep copy is a whole area of study. Rust does it very cleanly and infers a lot of busy work, but it still needs explicit instruction from the user as to what to copy how every now and then.

6

u/NFriik 17h ago edited 16h ago

Well, it kinda is, because that's not how Python works. A function is an ordinary object, it's default argument values are stored in a dict inside this object, so you can read and manipulate them at runtime. That wouldn't work with your idea, not without huge overhead.

-1

u/ZylonBane 16h ago

"That wouldn't work because that's not how it works" is a circular argument. The implementers could have made default values evaluate at call time, like JavaScript does, but instead they chose violence.

4

u/NFriik 16h ago

Implementing functions the way Python does is a design decision with advantages and disadvantages. One of the advantages is that it allows to adhere to one of the language's main principles, which is that everything is an object. In the end, this behavior is just a consequence of this. Having default arguments treated differently to other function attributes (such as the name, doc string and code) would be weird. The Python devs didn't choose violence, they chose consistency.

1

u/Loading_M_ 10h ago

A more important point: default values don't have to be stored on the function object at all. The only information actually needed is whether the keyword argument has a default value, not the specific value. Then the code to instantiate the default values (if needed) can simply be inserted into the function body before the user code. This is basically how most other interpreted languages handle it.

Alternatively, the default value could always be stored as an Expression object or similar.

82

u/_killer1869_ 19h 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.

22

u/Double_Cost4865 18h 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.

8

u/marinuso 11h ago

I've seen it used in production code to memoize functions in this manner:

def fib(n, cache = {0: 1, 1: 1}):
    if not n in cache:
	    cache[n] = fib(n-1) + fib(n-2)
    return cache[n]

Except with much more complicated code rather than this simple example.

1

u/bigorangemachine 10h ago

that's actually kinda smart

1

u/bigorangemachine 10h ago

default params is the classic case of IDGAF with code.

"I don't which array... just add it to an array"

12

u/Thelmholtz 18h ago

It's still a horribly funny feature.

6

u/CagoSuiFornelli 15h ago

Such code would never appear in actual code intended to be used.

You should take a tour in my company codebase

1

u/scheimong 5h ago

Such code would never appear in actual code intended to be used.

Tell me you're a junior dev without telling me you're a junior dev.

14

u/mothzilla 18h ago

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

4

u/Beregolas 18h 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 16h 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?

3

u/Beregolas 16h 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.

3

u/RngdZed 17h 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?

4

u/bigorangemachine 17h ago

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

1

u/lolcrunchy 14h 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))

1

u/bigorangemachine 10h ago

Ya I test in JS and JS will do this.. oddly enough they are so similar JS didn't do the thing OP posted about.

Surprised about that one... but it makes sense.

1

u/bigorangemachine 18h ago edited 17h ago

I bet this happens in JS as well

Edit: nope python

I should check gdscript

1

u/Crafty-Detail-3788 12h ago

Issue here is that the default parameters are initialized ONCE. If you want an empty list every time you need to define it inside.

1

u/itemluminouswadison 11h ago

It's a quirk of the language and something you should have learned and read about. The default arg gets created once upon func definition, not per invocation

1

u/Kadabrium 8h ago

Python doesnt do a lot of variable scoping for how much indent it does

1

u/GoddammitDontShootMe [ $[ $RANDOM % 6 ] == 0 ] && rm -rf / || echo “You live” 7h ago

So each call reuses the same default list? Yeah, I think I would've expected it to get passed an empty list by default each time.

1

u/Giiiin 5h ago

Yeah, I mean, don't do that

1

u/jmooroof2 18h ago

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

3

u/NFriik 17h 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.

5

u/simonsmine 18h 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 18h 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

1

u/4xe1 17h ago

default function parameters are useful, shared function default parameters are efficient, and mutable function parameter are convenient (saves the interpreter the check for mutability, and and saves the user the boiler plate to copy it in a local variable).

What you're seeing is merely a combination of the three.

But shared mutable function parameter is also uniquely useful for memoisation. It's not where you'd put the table of previous results in an other language, but it makes perfect sense to do so in python, as it hides a side effect which is not a real side effect.

1

u/awidesky 12h ago

Is that a documented behavior, or not?
If it is, that doesn't make sense.
If not, it's technically a bug, right?

0

u/Ollidav 19h ago

A mí me encanta el helado de vainilla.

0

u/akoOfIxtall 14h ago

I hate python from the bottom of my soul

0

u/OhItsJustJosh 14h 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

-3

u/CaptainFoyle 15h ago

Everyone knows: don't use mutable default arguments.

You just outed yourself as uninformed.

4

u/nakurtag 15h ago

Who you name "everyone"? Python developers? I'm not python developer, I'm from C#/Go/Rust world and this behavior is ridiculous for me. This is highly counterintuitive and illogical.

2

u/squabzilla 5h ago

My main language is Python, and still think it’s ridiculous how much unintuitive behaviour there is because Python’s array-equivalents, by default, copy the object-pointer instead of the object itself.

And then people tell you why it works like that, confusing an explanation for an excuse.

-1

u/CaptainFoyle 15h ago

I mean how do you learn a new language? Does it just pop into your head? Or do you look up documentation and syntax? Then that's usually one of the first things you read

3

u/nakurtag 15h ago

Eventually, I consulted the documentation when I encountered this behavior. But initially, I tried to write the code based on my experience as a programmer and by using deduction.