Live data from Hacker News

Python Anti-Pattern

valinsky.me

21–27 of 27 posts

Re: Python Anti-Pattern

#21
post #4

About half the bugs I fix is due to unexpected interactions with mutable state. Now, I am usually the creator of the bugs, and it might be that I completely lacks any intellectual rigour with regards to mutable state, but it is re-assuring that I am bot the only one. This specific behaviour of python has always struck me as completely idiotic.

> This specific behaviour of python has always struck me as completely idiotic I'm curious, what would be your proposed solution? Make a special case exception for when the expression that constitutes a function default is evaluated? What would that exception look like?

My solution would be to fix it and break backwards compatibility: Any default value is evaluated anew on each function call without the specified value. Then you CAN mutate it to your hearts desire without impacting subsequent invocations. If you want to keep hidden mutable state between function invocations you should use a closure.

    def banana(parts = []):
      pass
Calling that procedure without parts would mean parts would be bound to a NEW empty list, not the same old.

Every new pyhton programmer is bitten by this sooner or later. Most people seem to think it is stupid, and relying on it seems, to me, to go against the python mantra of "being explicit", where explicit would mean either using an instance variable in the case of OO or a closure.

Re: Python Anti-Pattern

#22

Earlier quoted context omitted.

Shout out to the amazing Pyright, the fast and feature-complete python LSP that you can use with any editor that supports the LSP protocol!

I'm using VSCode with different Python specific extensions, like Pylance which uses Pyright, but it didn't warn me about the mutable function parameter. RIP

You're right, I checked, and pyright only warns about function calls in default arguments, not mutable containers.

I started a discussion in Pyright's repo here[0].

[0] https://github.com/microsoft/pyright/discussions/2306

Re: Python Anti-Pattern

#23
post #7

Linters can help a ton with stuff like this, I think pylint warns against this footgun. Also is there a reason for not using the shorter: var = var or [] Instead of: var = [] if var is None else var

Both mypy and pyright/pylance do the right thing when things are guarded with explicit None checks and the wrong thing with “or”, so even though I prefer “or” otherwise when its gated upstrean so that None is the only falsey value it should get, I’ve taken recently to using explicit checks to make typecheckers happy.

That's not been my experience. I frequently use or way and have both mypy/pyright enabled. Testing this toy function,

from typing import List, Optional

def f(x: Optional[List[int]]): x = x or [] reveal_type(x)

mypy and pyright both show List[int] as type of x at the end and correctly drop None.

Re: Python Anti-Pattern

#24
post #21

Earlier quoted context omitted.

> This specific behaviour of python has always struck me as completely idiotic I'm curious, what would be your proposed solution? Make a special case exception for when the expression that constitutes a function default is evaluated? What would that exception look like?

My solution would be to fix it and break backwards compatibility: Any default value is evaluated anew on each function call without the specified value. Then you CAN mutate it to your hearts desire without impacting subsequent invocations. If you want to keep hidden mutable state between function invocations you should use a closure. def banana(parts = []): pass Calling that procedure without parts would mean parts w…

Indeed, when I asked for potential solutions, I thought it would be ... weird, to evaluate default args at call time, but it turns out that many other languages are like that[0], and the wierdness was my subjective perception shaped by my Python-heavy background.

Regarding breaking backward compatibility in a future release, I'm not qualified to weigh in on the design decision of balancing between the disadvantages of breaking backward compatibility and the advantage of more intuitive semantics.

[0] https://github.com/microsoft/pyright/discussions/2306#discus...

Re: Python Anti-Pattern

#25
post #21

Earlier quoted context omitted.

> This specific behaviour of python has always struck me as completely idiotic I'm curious, what would be your proposed solution? Make a special case exception for when the expression that constitutes a function default is evaluated? What would that exception look like?

My solution would be to fix it and break backwards compatibility: Any default value is evaluated anew on each function call without the specified value. Then you CAN mutate it to your hearts desire without impacting subsequent invocations. If you want to keep hidden mutable state between function invocations you should use a closure. def banana(parts = []): pass Calling that procedure without parts would mean parts w…

One useful case for the existing is to eagerly bind variables:

Here's a stupid example. Notice in the first case printing i doesn't grab the value of i when the lambda is defined. The second case is likely what you want.

  >>> lambdas = [lambda : print(i) for i in range(2)]
  >>> for l in lambdas:
  ...   l()
  ...
  1
  1
  >>> lambdas = [lambda i=i: print(i) for i in range(2)]
  >>> for l in lambdas:
  ...   l()
  ...
  0
  1

Re: Python Anti-Pattern

#26
post #21

Earlier quoted context omitted.

My solution would be to fix it and break backwards compatibility: Any default value is evaluated anew on each function call without the specified value. Then you CAN mutate it to your hearts desire without impacting subsequent invocations. If you want to keep hidden mutable state between function invocations you should use a closure. def banana(parts = []): pass Calling that procedure without parts would mean parts w…

One useful case for the existing is to eagerly bind variables: Here's a stupid example. Notice in the first case printing i doesn't grab the value of i when the lambda is defined. The second case is likely what you want. >>> lambdas = [lambda : print(i) for i in range(2)] >>> for l in lambdas: ... l() ... 1 1 >>> lambdas = [lambda i=i: print(i) for i in range(2)] >>> for l in lambdas: ... l() ... 0 1

Oh my, that's awful. Even in python there are better ways to close over a value :)

I haven't written python in many years, but in the following example it is at least clear what is going on:

   def make_printer(n):
       def printer():
           print(n)
       return printer
   [make_printer(i) for i in range(2)]

Re: Python Anti-Pattern

#27

Earlier quoted context omitted.

I'm using VSCode with different Python specific extensions, like Pylance which uses Pyright, but it didn't warn me about the mutable function parameter. RIP

You're right, I checked, and pyright only warns about function calls in default arguments, not mutable containers. I started a discussion in Pyright's repo here[0]. [0] https://github.com/microsoft/pyright/discussions/2306

Thanks to the maintainer's great responsiveness, Pyright will implement such a warning soon: https://github.com/microsoft/pyright/issues/2308
Post reply on HN