← Back to context

Comment by throwawaymaths

5 months ago

your comment assumes that features and functionality are a good thing. "worse is better" does not apply here.

"worse is better" is people putting up with footguns like this in python, because it's percieved easier to find a python job:

    def fun(a = []):

HN is very much "less is better", not "worse is better".

I'm not sure what you mean? The literal quote from the Wikipedia article on "worse is better" is:

> It refers to the argument that software quality does not necessarily increase with functionality: that there is a point where less functionality ("worse") is a preferable option ("better") in terms of practicality and usability.

For that reason, I think I am applying the term precisely as it was defined.

The irony of my comment, which dang picked up, is that the original idea was a criticism against Lisp, suggesting that the bloat of features was a part of the reason its adoption had lagged behind languages like C.

1. https://en.wikipedia.org/wiki/Worse_is_better

  • You're both saying the same thing: fewer features = higher quality.

    Swiss army knives are not as good at being screwdrivers as screwdrivers are.

    • In general: yes. But I’ve certainly had to use swathes of screwdrivers that are worse at being screwdrivers than my Swiss army knife is. Same I believe applies here: there’s a relation, but it’s nuanced. The same screwdriver is a better screwdriver when carried in a hand than in a toolbox full of other high-quality tools, but worse for everything else.

  • huh. til i actually thought "worse is better" is more recent than that but it stems from an era where feature count was the measure of quality. how times have changed!! thanks!

I've written Python for 14 years and have never seen code like that. It certainly isn't a perfect language, but this doesn't look like a common concern.

People write a lot of Python, because the language is easy to get into for a lot of non computer-science folks (e.g., engineers and scientists) and the ecosystem is massive with libraries for so many important things. It isn't as conceptually pure as lisp, but most probably don't care.

  • > I've written Python for 14 years and have never seen code like that.

    Exactly because it's a footgun that everybody hits very early. I think the Python linters even flag this.

    The fact that default arguments in Python get set to "None" is precisely because of this.

    • For this particular case, a better candidate is usually empty tuple () since it's actually iterable etc, so unless you need to mutate that argument...

      The bigger problem is with dicts and sets because they don't have the equivalent concise representation for the immutable alternative.

      Arguably the even bigger problem is that Python collection literals produce mutable collections by default. And orthogonal to that but contributing to the problem is that the taxonomy of collections is very disorganized. For example, an immutable equivalent of set is frozenset - well and good. But then you'd expect the immutable equivalent of list to be frozenlist, except it's tuple! And the immutable equivalent of dict isn't frozendict, it... doesn't actually exist at all in the Python stdlib (there's typing.MappingProxyType which provides a readonly wrapper around any mapping including dicts, but it will still reflect the changes done through the original dict instance, so to make an equivalent of frozenset you need to copy the dict first and then wrap it and discard all remaining references).

      Most of this can be reasonably explained by piecemeal evolution of the language, but by now there's really no excuse to not have frozendict, nor to provide an equally concise syntax for all immutable collections, nor to provide better aliases and more uniform API (e.g. why do dicts have copy() but lists do not?).

      2 replies →

  • It's a common need to have an empty array be the default value to an argument. In any programming language, really. I don't know what to make of the fact that you've never seen that in the wild.

    Maybe you were blessed with colleagues, for the past 14 years, that all know about how dangerous it is to do it in Python so they use workarounds? That doesn't negate the fact that it's a concern, though, does it?

    • There's always tension between language simplicity (and thus cognitive load of the programmers) and features. Compare Scheme with Common Lisp.

      The idea in Python is:

      1. Statements are executed line by line in order (statement by statement).

      2. One of the statements is "def", which executes a definition.

      3. Whatever arguments you have are strictly evaluated. For example f(g(h([]))), it evaluates [] (yielding a new empty list), then evaluates h([]) (always, no matter whether g uses it), then evaluates g(...), then evaluates f(...).

      So if you have

      def foo(x = []): ...

      that immediately defines

      foo = (lambda x = []: ...)

      For that, it has to immediately evaluate [] (like it always does anywhere!). So how is this not exactly what it should do?

      Some people complain about the following:

          class A:
              x = 3
              y = x + 2
      

      That now, x is a class variable (NOT an instance variable). And so is y. And the latter's value is 5. It doesn't try to second-guess whether you maybe mean any later value of x. No. The value of y is 5.

      For example:

          a = A()
          assert a.__class__.x == 3
          assert a.x == 3
          a.__class__.x = 10
          b = A()
          assert b.x == 10
      

      succeeds.

      But it just evaluates each line in the class definition statement by statement when defining the class. Simple!

      Complicating the Python evaluation model (that's in effect what you are implying) is not worth doing. And in any case, changing the evaluation model of the world's most used programming language (and in production in all countries of the world) in 2025 or any later date is a no go right there.

      If you want a complicated (more featureful) evaluation model, just use C++ or Ruby. Sometimes they are the right choice.

      7 replies →

    • > That doesn't negate the fact that it's a concern, though, does it?

      Yes, the fact that most people learn very early the correct way to have a constant value of a mutable type used when an explicit argument is not given and that using a mutable value directly as a default argument value uses a mutable value shared between invocations (which is occasionally desirable) means that the way those two things are done in Python isn't a substantial problem.

      (And, no, I don't think a constant mutable list is actually all that commonly needed as a default argument in most languages where mutable and immutable iterables share a common interface; if you are actually mutating the argument, it is probably not an optional argument, if you aren't mutating it, an immutable value -- like a python tuple -- works fine.)

  • I ran into this particular problem specifically because I wrote a ton of Racket that had this exact pattern and didn't see why Python should be any different. It really is a head scratcher in many ways the first time you run into it, IMO. I'm not sure I would immediately catch exactly what was going on even a decade later after I first discovered it.

Python made a choice to have default values instead of default expressions and it comes with positive and negative trade-offs. In languages like Ruby with default expressions you get the footgun the other way where calling a function with a default parameter can trigger side effects. This kind of function is fine in Python because it's unidiomatic to mutate your parameters, you do obj.mutate() not mutate(obj).

So while it's a footgun you will be writing some weird code to actually trigger it.

  • >In languages like Ruby with default expressions you get the footgun the other way where calling a function with a default parameter can trigger side effects.

    Seems fine to me. If the default expression causes side effects, then that's what I would expect.

    >This kind of function is fine in Python because it's unidiomatic to mutate your parameters, you do obj.mutate() not mutate(obj).

    I first wrote Python over 10 years ago and I never learned this.

    How would you idiomatically write a function/method which mutates >1 parameter?

    • It's just plain wrong. For example, next() is a builtin function which mutates the iterator passed to it. And, in general, given that Python doesn't have extension methods or anything similar, if you want to write a helper that works on mutable objects of some type, it'll have to be a free function.

      2 replies →

    • They are referring to a convention, not a language restriction.

      If you want to mutate two parameters just pass them to a function like you normally would.

      It's sloppy and a bad habit, I would not let it pass a PR in production code. Probably OK for a throwaway script.

Ah yes, the ol' default empty list Python gotcha, it bit me I think about 10 years ago, and ever since, sadly I've written code like this so many times it's not funny:

    def fun(a = None):
        _a = a if a is not None else []