top | item 10366293

Python wats

123 points| luu | 10 years ago |github.com | reply

122 comments

order
[+] ak217|10 years ago|reply
Of these, only += on tuple elements and indexing with floats strike me as WTFs, and many others are evidence of the author really going out of their way to look for behaviors to nitpick on. The absolute majority of these are things that Python programmers either will never encounter, or can easily explain from Python "first principles" (e.g. most of the nitpicks on string and sequence behaviors).

I can tell you what I consider WTF worthy - things that I and others have undoubtedly been bitten by because they are ubiquitous. Those are mutable defaults, lack of loop scoping, and "global"/"nonlocal" scoping. Oh, and add the crazy "ascii" I/O encoding default on 2.7 (and the inability to cleanly reset it) to that list.

[+] crdoconnor|10 years ago|reply
Truthiness values are also WTF worthy (although not mentioned above).

    if not x:
        do_something()
Where do_something will occur if x is None, zero, 0.0, a blank string or even midnight in some versions of python.

This has bitten me multiple times. It's a violation of python's explicit over implicit. It's an example of weak typing in an otherwise strongly typed language. There's really no good reason for it.

IMO, if x isn't a boolean, that line should just throw an exception. If you want to check for x being 0.0 or "", you should do x == "" or x == 0.0.

[+] ketralnis|10 years ago|reply
+= on lists is an annoying one:

    def myfunc(mylist):
        mylist += [1,2,3]
is NOT the same as:

    def myfunc(mylist):
        mylist = mylist + [1,2,3]
Try this on each of those definitions:

    a = [4,5,6]
    myfunc(a)
    print a
The first one mutates 'a', the second only reassigns (and discards) a temporary `mylist`.

Put another way, += is not just syntactic sugar. With lists, it actually mutates the original list.

[+] hueving|10 years ago|reply
The indexing with a float was on a dict. If you can index with a string, why not a float? If it's hashable, it's good!
[+] ngoldbaum|10 years ago|reply
The += on tuple elements thing kind of looks like a bug in CPython.
[+] aexaey|10 years ago|reply
3 different kinds of NaNs is pretty rad too. No idea what's happening here:

   >>> x = 0*1e400
   >>> set({x, x, float(x), float(x), 0*1e400, 0*1e400})
   set([nan, nan, nan])
   >>> set({x, float(x), 0*1e400, 0*1e400})
   set([nan, nan, nan])
   >>> set({x, float(x), 0*1e400})
   set([nan, nan])
EDIT: it gets worse:

   >>> x = 0*1e400
   >>> y = 0*1e400
   >>> z = 0*1e400
   >>> set({x, x, x})
   set([nan])
   >>> set({x, y, z})
   set([nan, nan, nan])
[+] bpicolo|10 years ago|reply
Yeah, things liked the nitpick on (1,2,3) == sorted((1,2,3)) seem not really `wats`. sorted returns a list, surprise surprise.

The only actual surprise on this list is, I agree, that += operator. Especially because tup[0] = tup[0] + [1] fails as expected.

[+] cma|10 years ago|reply
Bool acting as an integer instead of requiring a conversion (True//(True + True) == False*25).
[+] ris|10 years ago|reply
I would argue that the vast majority of behaviours described here are precisely the desired behaviour.

In fact, I think the ones he picks reflect tremendously on his skill as a programmer.

Take for instance his apparent insistence that int(2 * '3') should produce 6. Cool. Magic "special" behaviour for the multiply operator if the string happens to contain a number.

The same goes for his desire for the string "False" to be falsey. Wonderful magic special behaviour dependent on the (possibly unknown) content of a string means I've got to go round checking my strings for magic values.

[+] d23|10 years ago|reply
I agree, but there's enough weird shit going on here that it's still a worthwhile "article". For me, the `mixing numerical types`, `Operator precedence?`, `extend vs +=`, `Indexing with floats`, `all and emptiness`, and `Comparing NaNs` were all interesting.
[+] polemic|10 years ago|reply
I don't find many of these wat-worthy.

In python 2.x you could redefine True and False, and this is fun:

    >>> object() < object()
    True
    >>> object() < object()
    False
    >>> object() < object()
    True
But python3 dropped a lot of wat-ness.

This one is fun too: https://bugs.python.org/issue13936

[+] yokohummer7|10 years ago|reply
I interpret "wat" in the original "wat" presentation as "certain behaviors that can't be predicted by reading the code". In this regard, I don't think many things pointed out in the article can be considered as "wat". I could predict the behavior of many items fairly easily. The same thing could not be applied when I watched the original "wat" presentation.

The only exception I think is the bool coercion. Even though I can predict the behavior, I find it fairly disturbing. I believe that should have been removed when transitioning to Python 3. It is very unfortunate, considering Python is generally reluctant to implicit type conversion.

[+] pvdebbe|10 years ago|reply
Besides the mentioned debunks of wat, the

   all([[[]]]) == True
makes perfect sense, because the all all does is check the truthiness of the argument's elements. An empty list is false, non-empty is true, no matter the contents.
[+] agf|10 years ago|reply
By itself, yes, but

  >>> all([[[]]]) == True
  True
  >>> all([[]]) == True
  False
  >>> all([]) == True
  True
is pretty counter-intuitive at first glance.
[+] jayflux|10 years ago|reply
Can someone explain why the first one is a wat? To me it seems perfectly logical

Cast false to a string, then check the Boolean value of that string. It's not an empty string so of course it would be true.

[+] lostmsu|10 years ago|reply
Because people from normal langs or without cs background expect type cast roundtrip to return original value if temp type can represent any value of source type (e.g. string can represent any value of bool)
[+] ngoldbaum|10 years ago|reply
I have a favorite NumPy wat:

   >>> import numpy as np
   >>> 'x'*np.float64(3.5)
   'xxx'
(on recent NumPy releases this raises a deprecation warning)

Clearly the correct answer is 'xxx>' ;)

[+] aldanor|10 years ago|reply
How about

>>> type(np.uint64(1) + 1) numpy.float64

[+] jaibot|10 years ago|reply
Okay, I don't get this one:

  >>> False == False in [False]
  True
[+] tcdent|10 years ago|reply
I tend to write complex conditionals with parenthesis, sometimes excessively, but the clarity and guarantee of correct evaluation far outweighs any brevity.

    >>> False == (False in [False, ])
    False
[+] hueving|10 years ago|reply
The very first example is stupid. What sane language would assume a non-empty string should be converted to a False boolean?
[+] msane|10 years ago|reply
isn't it doing the opposite? str(False) is the string "False", which bool() casts to True because it is a non-empty string; Non-empty strings are cast to True booleans.

This is much faster than checking if the string has some semantic meaning first, which would have to be localized.

[+] agf|10 years ago|reply
I thought this was going to be about the "Investigating Python Wats" talk Amy Hanlon (my co-worker) gave at PyCon last year: https://www.youtube.com/watch?v=sH4XF6pKKmk
[+] dalke|10 years ago|reply
You might post this as an HN submission.
[+] nv-vn|10 years ago|reply
It seems like these all are exclusive to dynamically/weakly typed languages. Anyone have examples of wats for languages with strong static typing?
[+] Macha|10 years ago|reply
Java:

* Reassigning interned integers via reflection

* private fields work on a class level, not an instance level, i.e. Instances of a class can read private fields of other instances of the same class.

* Arguably package-private fields as the docs like to avoid mentioning they exist.

* ==, particularly in regards to boxed types.

* List<String> x = new ArrayList<String>() {{ add("hello"); add("world"); }};

* The behaviour of .equals with the object you create above

* Type Erasure

[+] oct|10 years ago|reply
OCaml's parsing rules are pretty opaque sometimes. For example, if you have an "if" statement that's just performing effects (not returning a value), you may run into behavior like this:

    # let x = ref 0;;
    val x : int ref = {contents = 0}
    # let y = ref 0;;
    val y : int ref = {contents = 0}
    # if false then
        x := 1;
        y := 1;
      ;;
    - : unit = ()
    # (!x, !y);;
    - : int * int = (0, 1)
    # y := 0;;
    - : unit = ()
    # if false then
        let z = 1 in
        x := z;
        y := z;
      ;;
    - : unit = ()
    # (!x, !y);;
    - : int * int = (0, 0)
[+] Ao7bei3s|10 years ago|reply
Sure. Javas signed byte type comes to mind.

Oh, and `null`, in any language ;-)

[+] 746F7475|10 years ago|reply
To me the first two: "Converting to a string and back" and "Mixing integers with strings" aren't "wats" at all.
[+] lqdc13|10 years ago|reply
actually almost none of these seem like wats to me.. Just what I would expect.

The one wat was adding a float to a huge integer. Granted, I assumed something fishy might happen with such large numbers, so I steer away from doing those operations.

[+] khulat|10 years ago|reply
Well to be fair it is only a wat if you don't know about floating point imprecision. Because that is exactly what is causing this contrary to what is stated. Also python just gets this behavior from C, so the other claim that this is a special case of python is also not true.

Since the 1 is shifted 53 bits to the left the number is right at the start of the number range where only even numbers can be represented.

You get 900719925470993 as the decimal number which you cast to float : 900719925470992.0

Then you add 1.0 and get 900719925470992.0 because of floating point imprecision and rounding. The next floating point number would be 900719925470994.0.

92 is less than 93 and this gets you this seemingly weird x+1.0 < x = true.

[+] eru|10 years ago|reply
> all and emptiness [...]

If all would work any other way, it would be seriously broken. (Given the rules for how to convert list to bool.)

[+] ksikka|10 years ago|reply
Anyone know the story behind the converse implication operator? What it's useful for, and why it's undocumented?
[+] johnsoft|10 years ago|reply
∗∗ is the exponentiation operator. True is implicitly converted to 1, and False to 0. It's a coincidence that 0^0, 0^1, 1^0, and 1^1 line up with converse implication.
[+] mxmul|10 years ago|reply
Remember that booleans are integers in python. This is just the regular exponential operator.
[+] dllthomas|10 years ago|reply
Working in Python, my biggest wat is that a test can cause a failed assertion in the code and still succeed (if AssertionError is caught somewhere upwards in the code, which will happen at any `except Exception`).
[+] ketralnis|10 years ago|reply
I've hit this too, it would be really nice if AssertError didn't inherit from Exception. There's a whole Exception object hierarchy but it seems like none of the parents of Exception are actually useable for anything
[+] eru|10 years ago|reply
That's why you should inject defects into your code, to see whether your tests catch them. There are tools to do that automatically.
[+] amelius|10 years ago|reply
I always run into trouble in Python with closures and assignment. E.g.,

  def f():
      x = 0
      def g():
          x += 1
      g()
  f()
[+] Veedrac|10 years ago|reply
Here's a quick interpretation of Python's method:

Any assignment operator (compound or not) follows the same namespace rules. Scope lookups are consistent within a scope, regardless of order. Any "namespaced" lookup (`x[y]` and `foo.bar`) will act inside that namespace.

Non-namespaced identifiers will always assign into the nearest enclosing scope (a scope is always a named function) by default, since clobbering outer scopes - particularly globals - is dangerous. You can opt in to clobbering with `global` and `nonlocal`. If you don't ever assign to a name in the current scope, lookups will be lexical (since a local lookup cannot succeed).

---

Hopefully most of these value judgements should be relatively obvious.

[+] fela|10 years ago|reply
the behavior of is seems pretty hard to predict

  >>> 3.1 is 3.1
  True
  >>> a = 3.1
  >>> a is 3.1
  False
[+] fela|10 years ago|reply
It seems to be a case of leaky abstraction, where the way python caches small objects becomes visible, another example:

  >>> a = 'hn'
  >>> a is 'hn'
  True
  >>> a = ''.join(['h', 'n'])
  >>> a is 'hn'
  False
  >>> a
  'hn'
  >>> a = 'h' + 'n'
  >>> a is 'hn'
  True
Edit: found another interesting case

  >>> a = 1
  >>> b = 1
  >>> a is b
  True
  >>> a = 500
  >>> b = 500
  >>> a is b
  False
[+] Veedrac|10 years ago|reply
`is` means "do these things reside in the same location in memory?", and the runtime is merely required to give a plausible answer.

For inequivalent values or `True`, `False` and `None`, the result is defined. Identity is also preserved across assignment. For everything else, the answer is "whatever the runtime feels like". PyPy, for instance, will always say True for integers and floats.

It's not just hard to predict - it's inherently unpredictable. The runtime gets free reign as long as you can't prove that it's lying.

[+] chris_wot|10 years ago|reply
How are the converse implication operators worthy of a wat? That's completely normal...
[+] misiti3780|10 years ago|reply
I thought I knew python .... I guess I do not know python ...

awesome idea - thank you