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
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
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
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
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
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/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
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.

94
u/drislands 19h ago
I would have expected this function to create a new empty list on any call where
itemsis not given. Doesn't this behavior make the function into a closure?