├── .gitignore ├── README.md ├── explanation.md ├── quiz-answers.md └── quiz.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python wats 2 | 3 | A "wat" is what I call a snippet of code that demonstrates a counterintuitive edge case of a programming language. (The name comes from [this excellent talk by Gary Bernhardt](https://www.destroyallsoftware.com/talks/wat).) If you're not familiar with the language, you might conclude that it's poorly designed when you see a wat. Often, more context about the language design will make the wat seem reasonable, or at least justified. 4 | 5 | Wats are funny, and learning about a language's edge cases probably helps you with that language, but I don't think you should judge a language based on its wats. (It's perfectly fine to judge a language, of course, as long as it's an *informed* judgment, based on whether the language makes it difficult for its developers to write error-free code, not based on artificial, funny one-liners.) This is the point I want to prove to Python developers. 6 | 7 | If you're a Python developer reading these, you're likely to feel a desire to put them into context with more information about how the language works. You're likely to say "Actually that makes perfect sense if you consider how Python handles *X*", or "Yes that's a little weird but in practice it's not an issue." And I completely agree. Python is a well designed language, and these wats do not reflect badly on it. You shouldn't judge a language by its wats. 8 | 9 | ## The Python wat quiz 10 | 11 | Are you unimpressed by these wats? Do you think these edge cases are actually completely intuitive? Try your hand at [the Python wat quiz](https://github.com/cosmologicon/pywat/blob/master/quiz.md)! 12 | 13 | ## The wats 14 | 15 | ### The undocumented [converse implication](https://en.wikipedia.org/wiki/Converse_implication) operator 16 | 17 | ```python 18 | >>> False ** False == True 19 | True 20 | >>> False ** True == False 21 | True 22 | >>> True ** False == True 23 | True 24 | >>> True ** True == True 25 | True 26 | ``` 27 | 28 | ### Mixing numerical types 29 | 30 | ```python 31 | >>> x = (1 << 53) + 1 32 | >>> x + 1.0 < x 33 | True 34 | ``` 35 | 36 | Think this is just floats being weird? Floats are involved, but it's not their fault this time. [Check out the explanation to see why.](https://github.com/cosmologicon/pywat/blob/master/explanation.md#mixing-numerical-types) 37 | 38 | ### Operator precedence? 39 | 40 | ```python 41 | >>> (False == False) in [False] 42 | False 43 | >>> False == (False in [False]) 44 | False 45 | >>> False == False in [False] 46 | True 47 | ``` 48 | 49 | [Source](https://www.reddit.com/r/programming/comments/3cjjgp/why_does_return_the_string_10/csxak65). 50 | 51 | ### Iterable types in comparisons 52 | 53 | ```python 54 | >>> a = [0, 0] 55 | >>> (x, y) = a 56 | >>> (x, y) == a 57 | False 58 | ``` 59 | 60 | ```python 61 | >>> [1,2,3] == sorted([1,2,3]) 62 | True 63 | >>> (1,2,3) == sorted((1,2,3)) 64 | False 65 | ``` 66 | 67 | [See the explanation if you think this is not a wat.](https://github.com/cosmologicon/pywat/blob/master/explanation.md#iterable-types-in-comparisons) 68 | 69 | ### Types of arithmetic operations 70 | 71 | The type of an arithmetic operation cannot be predicted from the type of the operands alone. You also need to know their value. 72 | 73 | ```python 74 | >>> type(1) == type(-1) 75 | True 76 | >>> 1 ** 1 == 1 ** -1 77 | True 78 | >>> type(1 ** 1) == type(1 ** -1) 79 | False 80 | ``` 81 | 82 | ### Fun with iterators 83 | 84 | ```python 85 | >>> a = 2, 1, 3 86 | >>> sorted(a) == sorted(a) 87 | True 88 | >>> reversed(a) == reversed(a) 89 | False 90 | ``` 91 | 92 | ```python 93 | >>> b = reversed(a) 94 | >>> sorted(b) == sorted(b) 95 | False 96 | ``` 97 | 98 | ### Circular types 99 | 100 | ```python 101 | >>> isinstance(object, type) 102 | True 103 | >>> isinstance(type, object) 104 | True 105 | ``` 106 | 107 | [Source](https://www.reddit.com/r/Python/comments/3c344g/so_apparently_type_is_of_type_type/csrwwyv). 108 | 109 | ### `extend` vs `+=` 110 | 111 | ```python 112 | >>> a = ([],) 113 | >>> a[0].extend([1]) 114 | >>> a[0] 115 | [1] 116 | >>> a[0] += [2] 117 | Traceback (most recent call last): 118 | File "", line 1, in 119 | TypeError: 'tuple' object does not support item assignment 120 | >>> a[0] 121 | [1, 2] 122 | ``` 123 | 124 | ### Indexing with floats 125 | 126 | ```python 127 | >>> [4][0] 128 | 4 129 | >>> [4][0.0] 130 | Traceback (most recent call last): 131 | File "", line 1, in 132 | TypeError: list indices must be integers, not float 133 | >>> {0:4}[0] 134 | 4 135 | >>> {0:4}[0.0] 136 | 4 137 | ``` 138 | 139 | ### `all` and emptiness 140 | 141 | ```python 142 | >>> all([]) 143 | True 144 | >>> all([[]]) 145 | False 146 | >>> all([[[]]]) 147 | True 148 | ``` 149 | 150 | ### `sum` and strings 151 | 152 | ```python 153 | >>> sum("") 154 | 0 155 | >>> sum("", ()) 156 | () 157 | >>> sum("", []) 158 | [] 159 | >>> sum("", {}) 160 | {} 161 | >>> sum("", "") 162 | Traceback (most recent call last): 163 | File "", line 1, in 164 | TypeError: sum() can't sum strings [use ''.join(seq) instead] 165 | ``` 166 | 167 | ### NaN-related wats 168 | 169 | ```python 170 | >>> x = float("nan") 171 | >>> y = float("nan") 172 | >>> x == x 173 | False 174 | >>> [x] == [x] 175 | True 176 | >>> [x] == [y] 177 | False 178 | ``` 179 | 180 | [Source](https://github.com/cosmologicon/pywat/issues/22). 181 | 182 | ```python 183 | >>> x = float("nan") 184 | >>> len({x, x, float(x), float(x), float("nan"), float("nan")}) 185 | 3 186 | >>> len({x, float(x), float("nan")}) 187 | 2 188 | ``` 189 | 190 | ```python 191 | >>> x = float("nan") 192 | >>> (2 * x).imag 193 | 0.0 194 | >>> (x + x).imag 195 | 0.0 196 | >>> (2 * complex(x)).imag 197 | nan 198 | >>> (complex(x) + complex(x)).imag 199 | 0.0 200 | ``` 201 | 202 | ## Explanations 203 | 204 | Want to learn more about the inner workings behind these wats? Check out the [explanation page](https://github.com/cosmologicon/pywat/blob/master/explanation.md) for clarity. 205 | -------------------------------------------------------------------------------- /explanation.md: -------------------------------------------------------------------------------- 1 | # Python wat explanation 2 | 3 | This page provides explanations for the details behind [the Python wats](https://github.com/cosmologicon/pywat/blob/master/explanation.md). 4 | 5 | 6 | ### The undocumented [converse implication](https://en.wikipedia.org/wiki/Converse_implication) operator 7 | 8 | ```python 9 | >>> False ** False == True 10 | True 11 | >>> False ** True == False 12 | True 13 | >>> True ** False == True 14 | True 15 | >>> True ** True == True 16 | True 17 | ``` 18 | 19 | This is not actually an undocumented feature. `**` is simply the exponent operator, and `False` and `True` are simply the numbers `0` and `1`. In mathematics, 0^0 = 1, 0^1 = 0, etc. It just happens to work out that the truth table is the same as for the converse implication operator. 20 | 21 | ### Mixing numerical types 22 | 23 | ```python 24 | >>> x = (1 << 53) + 1 25 | >>> x + 1.0 < x 26 | True 27 | ``` 28 | 29 | Let's call the real number N = 2^53 + 1 = 9007199254740993. Despite what you might have heard, floating-point representation does *exactly* represent a large range of integers, just like integer types do. (If that's surprising to you, see [this excellent paper by David Goldberg](https://ece.uwaterloo.ca/~dwharder/NumericalAnalysis/02Numerics/Double/paper.pdf).) N is significant in that it's the smallest whole number that is not represented exactly by a Python `float`, which has 52 bits of precision. Of course, N can be represented exactly by a Python `int`. 30 | 31 | So let's look at the value that `x` holds. The right-hand side `(1 << 53) + 1` is an integer expression that's equal to N, so `x` gets the value N. If `x` is converted to a float with `float(x)`, then the result cannot be N exactly, since floats can't represent that value. According to rounding rules, the value gets converted to N - 1, which floats *can* represent. 32 | 33 | So what is the value of `x + 1.0`? First, in order to perform the addition, `x` (which is an int containing the value N) is converted to a float. The result of this conversion is a float equal to N - 1. Next `1.0` is added, bringing the total value back to N. Finally, this value is coerced into a float again, bringing it back to N - 1. When `x + 1.0 < x` is evaluated, the left side is a float containing the value N - 1, and the right is an int containing the value N. The line is essentially this: 34 | 35 | 9007199254740992.0 < 9007199254740993 36 | 37 | There are three options that programming languages take when asked to perform this comparison. First, they may throw an error, as in Haskell, which refuses to compare floating-point types to integer types. Second, they may convert the right-hand side to float, in which case the right-hand side gets converted to N - 1, just like the left, and the comparison evaluates to false. This is the option taken by most popular languages, including C, C++, and Java. 38 | 39 | Python and Ruby, however, take a third option, which involves more complicated logic, designed to catch exactly this case. In Python and Ruby, the result of a comparison will be the comparison of the actual numerical values, even if one side cannot be accurately represented with an integer, and the other side cannot be accurately represented with a float. 40 | 41 | Again, this is not just floats being weird (and if you really think floats behave weirdly, I highly recommend that Golberg paper). This wat does not happen in most languages that use floats, and it doesn't happen when you stick with floats: 42 | 43 | ```python 44 | >>> x = float((1 << 53) + 1) 45 | >>> x + 1.0 < x 46 | False 47 | ``` 48 | 49 | It only happens when you mix floats with ints, and only if you have Python or Ruby's complicated comparison logic. 50 | 51 | ### Operator precedence? 52 | 53 | ```python 54 | >>> False == False in [False] 55 | True 56 | ``` 57 | 58 | Neither the `==` nor the `in` happens first. They're both [comparison operators](https://docs.python.org/3.5/reference/expressions.html#comparisons), with the same precedence, so they're chained. The line is equivalent to `False == False and False in [False]`, which is `True`. 59 | 60 | ### Iterable types in comparisons 61 | 62 | Lists and tuples do not compare equal to each other, even if they contain the same elements. 63 | 64 | ```python 65 | >>> [] == () 66 | False 67 | ``` 68 | 69 | Some people have claimed to me that this is this is the way it should be, that different types are not supposed to compare equal to each other. While there may be good reasons for lists and tuples specifically to not compare equal, the general claim that different types should not compare equal is very un-pythonic, in my humble opinion. There are many instances of different types comparing equal in Python. If you really insist that this one's not a wat because list and tuple are different types, please accept these 13 wats instead: 70 | 71 | ```python 72 | >>> True == 1 73 | True 74 | >>> True == 1.0 75 | True 76 | >>> True == 1j 77 | True 78 | >>> set([]) == frozenset([]) 79 | True 80 | >>> u"" == "" # Different types in Python 2 only. 81 | True 82 | >>> bytearray() == "" # Only true in Python 2. 83 | True 84 | >>> collections.OrderedDict() == dict() 85 | True 86 | >>> collections.defaultdict() == dict() 87 | True 88 | >>> collections.Counter() == dict() 89 | True 90 | >>> collections.namedtuple("x", "a")(1) == (1,) 91 | True 92 | >>> collections.UserList() == [] 93 | True 94 | >>> collections.UserString() == "" 95 | True 96 | >>> enum.IntEnum("x", "a").a == 1 97 | True 98 | ``` 99 | 100 | ### Fun with iterators 101 | 102 | ```python 103 | >>> a = 2, 1, 3 104 | >>> sorted(a) == sorted(a) 105 | True 106 | >>> reversed(a) == reversed(a) 107 | False 108 | ``` 109 | 110 | Unlike `sorted` which returns a list, `reversed` returns an iterator. Iterators compare equal to themselves, but not to other iterators that contain the same values. 111 | 112 | ```python 113 | >>> b = reversed(a) 114 | >>> sorted(b) == sorted(b) 115 | False 116 | ``` 117 | 118 | The iterator `b` is consumed by the first `sorted` call. Calling `sorted(b)` once `b` is consumed simply returns `[]`. 119 | 120 | ### `extend` vs `+=` 121 | 122 | ```python 123 | >>> a = ([],) 124 | >>> a[0].extend([1]) 125 | >>> a[0] 126 | [1] 127 | >>> a[0] += [2] 128 | Traceback (most recent call last): 129 | File "", line 1, in 130 | TypeError: 'tuple' object does not support item assignment 131 | >>> a[0] 132 | [1, 2] 133 | ``` 134 | 135 | [Explanation from the Python FAQ.](https://docs.python.org/3/faq/programming.html#why-does-a-tuple-i-item-raise-an-exception-when-the-addition-works) 136 | 137 | ### `all` and emptiness 138 | 139 | ```python 140 | >>> all([]) 141 | True 142 | >>> all([[]]) 143 | False 144 | >>> all([[[]]]) 145 | True 146 | ``` 147 | 148 | When converted too a bool, `[]` becomes `False` (since it's empty), and `[[]]` becomes `True` (since it's not empty). Therefore `all([[]])` is equivalent to `all([False])`, and `all([[[]]])` is equivalent to `all([True])`. The first case `all([])` is trivially `True`. 149 | 150 | ### NaN-related wats 151 | 152 | ```python 153 | >>> x = float("nan") 154 | >>> len({x, x, float(x), float(x), float("nan"), float("nan")}) 155 | 3 156 | >>> len({x, float(x), float("nan")}) 157 | 2 158 | ``` 159 | 160 | For the purpose of set membership, two objects `x` and `y` are considered equivalent if `x is y or x == y`. For two NaNs, this will be true whenever they're the same object. So `{x, x}` will have length `1`. Every separately-defined NaN is a different object, so `x is 0*1e400` is `False`, and `{x, 0*1e400}` will have length 2. Finally, `float` called on a NaN will return the same object, so `x is float(x)` is `True`, and `{x, float(x)}` will have length 1. 161 | -------------------------------------------------------------------------------- /quiz-answers.md: -------------------------------------------------------------------------------- 1 | # The Python wat quiz Answers 2 | 3 | ## STOP! SPOILER ALERT! 4 | 5 | [Click here if you haven't taken the quiz.](https://github.com/cosmologicon/pywat/blob/master/quiz.md) 6 | 7 | The quiz questions are repeated below along with their answers. At the very bottom there's a quick answer key if you just want to grade yourself quickly. 8 | 9 | ## Evaluate your score 10 | 11 | Count the number of questions out of 10 that you had the correct answer for. 12 | 13 | * 12/12: Congratulations! You are an expert at Python edge cases! 14 | * 11/12: Great job! You are proficient at Python edge cases! 15 | * 10/12: Not bad! You are skilled at Python edge cases! 16 | * 9/12 or less: This quiz is unable to distinguish your results from random chance with any statistical certainty. Sorry. 17 | 18 | Just remember that edge cases can be interesting, but knowing the exact behavior of Python edge cases is not the same as being a good Python programmer. If you follow good programming practices, you can avoid problems caused by edge cases, even ones you're not aware of. 19 | 20 | I hope this was fun, though. Thanks for trying it out! 21 | 22 | ## Questions with answers 23 | 24 | ### Question 1: `min` of two elements 25 | 26 | **Yes.** This snippet is possible. 27 | 28 | ```python 29 | >>> x, y = {0}, {1} 30 | >>> min(x, y) == min(y, x) 31 | False 32 | ``` 33 | 34 | ### Question 2: size of sets and lists 35 | 36 | **No.** This snippet is impossible. 37 | 38 | ```python 39 | >>> x = ??? 40 | >>> len(set(list(x))) == len(list(set(x))) 41 | False 42 | ``` 43 | 44 | 45 | ### Question 3: `type` vs `map` 46 | 47 | **Yes.** This snippet is possible. 48 | 49 | ```python 50 | >>> x, s = True, {1} 51 | >>> s.add(x) 52 | >>> type(x) in map(type, s) 53 | False 54 | ``` 55 | 56 | ### Question 4: `zip` vs comparison 57 | 58 | **Yes.** This snippet is possible. 59 | 60 | ```python 61 | >>> x, y = [], [0] 62 | >>> x < y and all(a >= b for a, b in zip(x, y)) 63 | True 64 | ``` 65 | 66 | ### Question 5: zero `sum` 67 | 68 | **No.** This snippet is impossible. 69 | 70 | ```python 71 | >>> x, y = ??? 72 | >>> sum(0 * x, y) == y 73 | False 74 | ``` 75 | 76 | ### Question 6: argument expansion 77 | 78 | **Yes.** This snippet is possible. 79 | 80 | ```python 81 | >>> x = [[0]] 82 | >>> min(x) == min(*x) 83 | False 84 | ``` 85 | 86 | ### Question 7: Associative multiplication 87 | 88 | **Yes.** This snippet is possible. 89 | 90 | ```python 91 | >>> x, y, z = [0], -1, -1 92 | >>> x * (y * z) == (x * y) * z 93 | False 94 | ``` 95 | 96 | ### Question 8: `max` vs `in` 97 | 98 | **Yes.** This snippet is possible. 99 | 100 | ```python 101 | >>> x, y = "aa", "aa" 102 | >>> y > max(x) and y in x 103 | True 104 | ``` 105 | 106 | ### Question 9: `any` vs addition 107 | 108 | **No.** This snippet is impossible. 109 | 110 | ```python 111 | >>> x, y = ??? 112 | >>> any(x) and not any(x + y) 113 | True 114 | ``` 115 | 116 | ### Question 10: `count` vs `len` 117 | 118 | **Yes.** This snippet is possible. 119 | 120 | ```python 121 | >>> x, y = "a", "" 122 | >>> x.count(y) <= len(x) 123 | False 124 | ``` 125 | 126 | ### Question 11: `all` vs `filter` 127 | 128 | **No.** This snippet is impossible. 129 | 130 | ```python 131 | >>> x = ??? 132 | >>> all(filter(None, x)) 133 | False 134 | ``` 135 | 136 | ### Question 12: `max` vs slice 137 | 138 | **No.** This snippet is impossible. 139 | 140 | ```python 141 | >>> x, a, b, c = ??? 142 | >>> max(x) < max(x[a:b:c]) 143 | True 144 | ``` 145 | 146 | ## Quick answer key 147 | 148 | 1. Yes 149 | 2. No 150 | 3. Yes 151 | 4. Yes 152 | 5. No 153 | 6. Yes 154 | 7. Yes 155 | 8. Yes 156 | 9. No 157 | 10. Yes 158 | 11. No 159 | 12. No 160 | 161 | -------------------------------------------------------------------------------- /quiz.md: -------------------------------------------------------------------------------- 1 | # The Python wat quiz 2 | 3 | Test your knowledge of Python edge cases! 4 | 5 | ## Instructions 6 | 7 | In each of the 12 snippets below, one or more values is missing (marked with `???`). In each case, answer Yes or No: is it possible to replace the `???` with an expression using basic, built-in Python types (see Details below) that will make the snippet into exactly what you would see if you entered the text into the interpreter? 8 | 9 | ## Examples 10 | 11 | ### Example question 1 12 | 13 | ```python 14 | >>> x, y = ??? 15 | >>> x + y == y + x 16 | False 17 | ``` 18 | 19 | ### Example answer 1 20 | 21 | **Yes.** This snippet is possible. 22 | 23 | ```python 24 | >>> x, y = [0], [1] 25 | >>> x + y == y + x 26 | False 27 | ``` 28 | 29 | ### Example question 2 30 | 31 | ```python 32 | >>> x = ??? 33 | >>> x < x 34 | True 35 | ``` 36 | 37 | ### Example answer 2 38 | 39 | **No.** This snippet is impossible. 40 | 41 | ## Details and scope 42 | 43 | Now, technically, `type("", (), {"__lt__": lambda a, b: True})()` would make Example question 2 work. But don't worry. This quiz is not about trick questions, and I'm not looking for "trick" answers like that. **The missing values in this quiz are limited to the built-in types `bool`, `int`, `list`, `tuple`, `dict`, `set`, `frozenset`, `str`, and `NoneType`, and values of these types.** 44 | 45 | In particular, the following are out of scope, and are not valid as missing values: 46 | 47 | * `float`s, including `nan` and `inf` 48 | * unicode (Strings, if they appear, only have ascii characters.) 49 | * the `type` keyword 50 | * user-defined classes 51 | * `lambda`s 52 | * anything using `globals` or `locals` 53 | * anything using `import` 54 | * anything that changes value just by inspecting it or iterating over it (iterators and generators) 55 | * expressions with side effects (e.g. `eval`), including anything that redefines built-ins (obviously) 56 | * `bytes` arrays, `buffer`s, `range`s, `memoryview`s, and dictionary views 57 | 58 | Again, these are not trick questions. For snippets whose answers are *Yes, this is possible*, the missing values will clearly be in scope. 59 | 60 | Assume the latest Python version (currently 3.5) if it matters. It shouldn't, though. 61 | 62 | ## The questions 63 | 64 | Make sure to write down your answers (Yes or No) *before* looking at the answers, so you're not tempted to cheat. 65 | 66 | For a *real* challenge, try taking the quiz without using a Python interpreter to check your answers. 67 | 68 | Good luck! 69 | 70 | ### Question 1: `min` of two elements 71 | 72 | ```python 73 | >>> x, y = ??? 74 | >>> min(x, y) == min(y, x) 75 | False 76 | ``` 77 | 78 | ### Question 2: size of sets and lists 79 | 80 | ```python 81 | >>> x = ??? 82 | >>> len(set(list(x))) == len(list(set(x))) 83 | False 84 | ``` 85 | 86 | ### Question 3: `type` vs `map` 87 | 88 | ```python 89 | >>> x, s = ??? 90 | >>> s.add(x) 91 | >>> type(x) in map(type, s) 92 | False 93 | ``` 94 | 95 | ### Question 4: `zip` vs comparison 96 | 97 | ```python 98 | >>> x, y = ??? 99 | >>> x < y and all(a >= b for a, b in zip(x, y)) 100 | True 101 | ``` 102 | 103 | ### Question 5: zero `sum` 104 | 105 | ```python 106 | >>> x, y = ??? 107 | >>> sum(0 * x, y) == y 108 | False 109 | ``` 110 | 111 | ### Question 6: argument expansion 112 | 113 | ```python 114 | >>> x = ??? 115 | >>> min(x) == min(*x) 116 | False 117 | ``` 118 | 119 | 120 | ### Question 7: Associative multiplication 121 | 122 | ```python 123 | >>> x, y, z = ??? 124 | >>> x * (y * z) == (x * y) * z 125 | False 126 | ``` 127 | 128 | ### Question 8: `max` vs `in` 129 | 130 | ```python 131 | >>> x, y = ??? 132 | >>> y > max(x) and y in x 133 | True 134 | ``` 135 | 136 | ### Question 9: `any` vs addition 137 | 138 | ```python 139 | >>> x, y = ??? 140 | >>> any(x) and not any(x + y) 141 | True 142 | ``` 143 | 144 | ### Question 10: `count` vs `len` 145 | 146 | ```python 147 | >>> x, y = ??? 148 | >>> x.count(y) <= len(x) 149 | False 150 | ``` 151 | 152 | ### Question 11: `all` vs `filter` 153 | 154 | ```python 155 | >>> x = ??? 156 | >>> all(filter(None, x)) 157 | False 158 | ``` 159 | 160 | ### Question 12: `max` vs slice 161 | 162 | ```python 163 | >>> x, a, b, c = ??? 164 | >>> max(x) < max(x[a:b:c]) 165 | True 166 | ``` 167 | 168 | ## Answers 169 | 170 | All done? Make sure you have your answers written down and [check out the answers here](https://github.com/cosmologicon/pywat/blob/master/quiz-answers.md). 171 | 172 | --------------------------------------------------------------------------------