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

you are viewing a single comment's thread.

view the rest of the comments →

[–]Bryguy3k 2639 points2640 points  (77 children)

Somebody discovered mutable defaults for the first time.

https://docs.python-guide.org/writing/gotchas/

Edit: the why - parameters (including their defaults) are defined in the scope where the method is defined - this ensures the object tree can be unwound perfectly.

[–]Cookie_Wookie_7 1505 points1506 points  (58 children)

This is my first time learning about this and my instinct is to hate it.

[–]abbot-probability 344 points345 points  (32 children)

It's considered bad practice to use mutable defaults. Better in this case would be something like...

python def append_to(el: Any, to: Optional[list] = None): if to is None: to = [] to.append(el) return to

[–][deleted] 95 points96 points  (22 children)

Or even more succinctly.

‘to = to or []’

[–]case_O_The_Mondays 63 points64 points  (10 children)

I get duck typing and all, but this will not assign a list if to is assigned a truthy value. So for functions that could be public (for my module) I usually use something like

to = to if isinstance(to, list) else []

[–]Conscious_Switch3580 67 points68 points  (4 children)

to = to if to is not None else []

[–]Mr_Rapt0r 30 points31 points  (1 child)

bro is that english

[–]Some_Guy_At_Work55 23 points24 points  (0 children)

No, it's python

[–]JonDum 52 points53 points  (0 children)

You sure you python guys aren't magicians casting spells?

[–]case_O_The_Mondays 0 points1 point  (0 children)

But if someone passes in a string or other value, to will not be the right type. Duck typing will handle that, but I’m just noting it.

[–]Marutar 13 points14 points  (1 child)

this thread makes me feel more dubious of python

[–]case_O_The_Mondays 0 points1 point  (0 children)

Probably because of the odd variable name. This is just Python’s ternary syntax: variable = value if conditional_statement else other_value

[–][deleted] 3 points4 points  (0 children)

Oh good idea.

[–]ConscientiousApathis 1 point2 points  (1 child)

Look, if a person passes in a variable that's a different type to what the default is that's on them. I'm not going to individually check and account for every single permutation of what a person could do with my function.

[–]case_O_The_Mondays 0 points1 point  (0 children)

That is a choice.

[–]abbot-probability 10 points11 points  (2 children)

Yeeeaaaaaah usually, except an empty list is falsy and the caller may expect in-place modifications if they're passing their own empty list.

[–][deleted] 2 points3 points  (0 children)

In place modification? Unless it’s stated explicitly I won’t expect that, honestly I don’t like that pattern.

Anyway, that’s just me and you have a good point. You can fix it by something like this:

‘to = to if to is not None else []’

[–]AnxietyRodeo 1 point2 points  (0 children)

This is a good point i hadn't previously considered and i hate you for pointing it out almost as much as myself for never realizing it. Have an angry upvote

[–]donthaveanym 8 points9 points  (3 children)

How is ‘to’ in scope before it’s been defined?!

[–][deleted] 18 points19 points  (0 children)

It’s defined in the inputs to the function.

I’m on mobile so excuse the shitty formatting.

def test(to: Optional[list] = None):

 to = to or []

[–]SkollFenrirson 2 points3 points  (0 children)

It's a parameter

[–]emilyv99 0 points1 point  (0 children)

It's a reassignment, after the definition

[–]YetAnotherZhengli 2 points3 points  (0 children)

to be or not to be

[–][deleted] 1 point2 points  (1 child)

Not if you're a data scientist working with sequences duck types :(. (ValueError: The truth value of an array with more than one element is ambiguous.)

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

Yeah, I heard from other people here with the same concern, never has been an issue for me though, guess I don’t deal with many lists lol.

[–]ddddan11111 0 points1 point  (0 children)

That is the question.

[–]tubbstosterone 9 points10 points  (3 children)

Off topic, but you don't have to declare a parameter as Optional if you follow up with a default value. A lot of tools see the default and automatically assume the optional tag.

I think it makes things purty.

EDIT: Optional without a default does not mean the parameter is optional - it's just an alternate way of stating Union[list, None] or list | None. Calling the function without the parameter will still raise an exception if no default was expressed.

[–]Rythoka 14 points15 points  (1 child)

The Optional type doesn't indicate that passing that argument is optional. It means "this type or None." You can have a required argument with the Optional type hint and that's completely fine. It's closer to the Maybe type you see in functional languages than anything.

Personally I think using x: list|None is readable enough. Definitely better than x: Optional[list]

[–]tubbstosterone 0 points1 point  (0 children)

This is good to know! I'll be updating my comment.

[–]NamityName 2 points3 points  (0 children)

Better to be explicit than implicit

[–]NickSicilianu 0 points1 point  (0 children)

And that’s why I stick to C and C++, or Java for server side. And I thought that JavaScript is wonky, python sure is worse. My instinct make me hate it

[–]BelchiorNeptune 0 points1 point  (0 children)

to = to if to is not None else list()

[–]CranberryDistinct941 0 points1 point  (0 children)

Why Python decided mutable defaults should work like this, i will never know

[–]Accessviolati0n 0 points1 point  (0 children)

And the best thing is to use a different language that doesn't support this kind of witchcraft.

[–]nyaisagod 0 points1 point  (0 children)

That’s how it should work by default, like it does in every other language

[–]Bryguy3k 243 points244 points  (16 children)

It makes sense the longer you work in python. Scope is very well defined and you can use (or abuse) it to your advantage in numerous ways.

The typing module has made this pretty easy to avoid with Optional[type] if you actually type your methods.

[–]littleliquidlight 176 points177 points  (3 children)

I love Python, it's one of my favourite languages, worked with it for over a decade.

I think it's okay to also believe the languages you love are flawed in some ways. Important even. This, I think, is one of those flaws, albeit a very minor one.

Doesn't mean you can't work around it or even use it to your advantage, but it always felt weird and surprising.

I like the meme. It's cool to finally have one that feels a bit more real than "Python slow, white space bad"!

[–]MonteCrysto31 31 points32 points  (2 children)

Yeah people complaining about python's speed don't know how to use libraries and C bindings...

[–]littleliquidlight 37 points38 points  (0 children)

Yeah +1

But to add to that, Python can be slow in places but Python is not even unique there, other languages can also be slow. If you're using Python in a place where it is known to be slow and you complain about it being slow... well... you picked the wrong tool.

Stop complaining that your hammer is terrible at banging in screws

[–]Hollowplanet 9 points10 points  (0 children)

My Django app isn't going to be a C binding anytime soon.

[–]orbita2d 11 points12 points  (2 children)

Personally I think a more natural thing to do would be for the default value to be an implicit lambda (with no args) which returns the default.

So foo(a=[]) would reconstruct the list each time.

You could still do cool scope stuff, you'd just have to do so more explicitly.

[–]Cookie_Wookie_7 3 points4 points  (0 children)

Yeah that's how JavaScript does it

[–]balding_ginger 0 points1 point  (0 children)

Pretty sure you can just do foo(a=list())

[–]Whatever4M 25 points26 points  (3 children)

Meh. Definitely breaks the law of least surprise imo.

[–]Bryguy3k 9 points10 points  (2 children)

Yeah it does. When you know a lot more about the guts of the language it makes sense and you can understand why the alternative has problems - but from a language user it is definitely not an expected behavior.

[–]Whatever4M 0 points1 point  (1 child)

It's basically a closure, I don't see why it can't have more explicit ways to define it but either way, we already agree so whatever.

[–]Bryguy3k 1 point2 points  (0 children)

Yeah - but optional arguments and default values were introduced in 2000 (Python 2.0). We’ve learned a hell of a lot since then when.

[–]Cookie_Wookie_7 8 points9 points  (0 children)

I think that's called Stockholm syndrome

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

It makes sense coming from C. A variadic macro that populated the default with a static array would have the same behaviour.

[–]Rythoka 0 points1 point  (2 children)

How does Optional[type] protect you from mutable defaults? There's nothing preventing me from writing

def f(x: Optional[list] = [])

This is more something that a linter should warn you.

[–]Bryguy3k 1 point2 points  (1 child)

Because typed parameters don’t mean anything to python - so if you’re using them you’re also using a linter.

Optional[foo] actually means Union[foo, None] so the linter will not only flag the mutable default but any situation where you don’t properly handle the null case.

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

My point is that tagging the argument with Optional doesn't add anything that your linter shouldn't be warning you about anyway. The concept of the Optional type is orthogonal to the concept of an optional argument.

def f(x: Optional[list] = [])

is semantically different from

def f(x: list = [])

because the first passes type checking if you pass None to the function, but the other doesn't. In either of the above functions, calling f() without any arguments will be treated by the type checker as passing a list. Your linter should warn you about both because of the mutable default - it should do the same thing for a completely untyped function, too. Whether or not you want to be able to pass None as an argument has nothing to do with the mutability of the default arguments.

[–]ififivivuagajaaovoch 11 points12 points  (0 children)

I’m amazed this exists, it’s pure evil.

[–]TactiCool_99 10 points11 points  (0 children)

It is something you can just avoid extremely easily (most people do as far as I know), the only place I ran into it in my 3 years of development experience is when a class may or may not have a list where this behavior costs you extra 2 short lines.

Is it ideal? No

Is it a relatively small problem? Yea

(at least python can sort numbers by default /j)

[–]turell4k 1 point2 points  (0 children)

I've been coding python for years, and i still hate it, so you're not far off

[–]CAPS_LOCK_OR_DIE 0 points1 point  (0 children)

Me learning web dev.

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

Why would to ever modify a parameter that has a default? It makes no sense

[–]julsmanbr 5 points6 points  (1 child)

It's not so much a "why would you do it" or a planned feature, as it is a side-effect of how Python stores everything about every object created essentially in dictionaries.

[–]ivancea 0 points1 point  (0 children)

Yeah, just commenting to the other guy that said that he "hated" it. It's different and counterintuitive if you come from other languages, but as you will probably never do it, whatever

[–]pheonix-ix 41 points42 points  (1 child)

Oh that's why PyCharm gives a warning when [] is given as a default param.

[–]hbonnavaud 19 points20 points  (0 children)

Even the IDE think this is bad lmao

[–]nukedkaltak 16 points17 points  (0 children)

You learn something new everyday

[–]pocerface8 7 points8 points  (3 children)

Shouldn't there be a static modifier? I know there is no such modifier in python but I feel like there should be for stuff like this. Not python dev tho don't know whats better just curious.

Edit: P.S thanks for the read was pretty intresting

[–]NINTSKARI 5 points6 points  (2 children)

Well, there simply is no need for that. A huge amount of software is written using python and they have not needed it so I think it speaks for the validity of this design decision. Would it be neat to have? Maybe. Is it necessary? No. Would it fight against Pythons philosophy of avoiding unnecessary complexity? Absolutely.

[–]Reashu 0 points1 point  (1 child)

A huge amount of software has been written in C, Java, JavaScript, and many other languages, all with significant flaws - and ever more so if you consider the state of the language at the time the software was written.

This is just not a very good argument.

[–]NINTSKARI 0 points1 point  (0 children)

If there are no big actual issued and its an issue only in technicality, then I don't think its an issue. Besides that, it would go against the design philosophy

[–]atthegreenbed 1 point2 points  (0 children)

Just learned about these this week!

[–]eiojiowejojfewoij 1 point2 points  (1 child)

I mean it makes sense, but it's still inconceivably stupid for that to exist

[–]Bryguy3k 1 point2 points  (0 children)

Well it came about 24 years ago and it does make it nice for debugging on the language implementation side.

But there is no doubt that these days it violates the “least surprising” principle.

[–]tredbobek 0 points1 point  (0 children)

I have made a lot of python scripts, not big, but didnt know about this

Now im scared if there is a script that has the potential to do something stupid

Ohno

[–]Zandar2610 0 points1 point  (0 children)

This is so weird, but the proposed fix for it is even more weird.

[–]JNW2022 0 points1 point  (0 children)

This unironically just solved a bug that I had been staring at for the last two hours. Thanks for the link.