GitHub - satwikkansal/wtfpython: What the f*ck Python? 😱 (original) (raw)

Shows a wtfpython logo.

What the f*ck Python! 😱

Exploring and understanding Python through surprising snippets.

Translations: Chinese 中文 | Vietnamese Tiếng Việt | Spanish Español | Korean 한국어 | Russian Русский | German Deutsch | Add translation

Other modes: Interactive Website | Interactive Notebook

Python, being a beautifully designed high-level and interpreter-based programming language, provides us with many features for the programmer's comfort. But sometimes, the outcomes of a Python snippet may not seem obvious at first sight.

Here's a fun project attempting to explain what exactly is happening under the hood for some counter-intuitive snippets and lesser-known features in Python.

While some of the examples you see below may not be WTFs in the truest sense, but they'll reveal some of the interesting parts of Python that you might be unaware of. I find it a nice way to learn the internals of a programming language, and I believe that you'll find it interesting too!

If you're an experienced Python programmer, you can take it as a challenge to get most of them right in the first attempt. You may have already experienced some of them before, and I might be able to revive sweet old memories of yours! 😅

PS: If you're a returning reader, you can learn about the new modifications here (the examples marked with asterisk are the ones added in the latest major revision).

So, here we go...

Table of Contents

Structure of the Examples

All the examples are structured like below:

▶ Some fancy Title

Set up the code.

Preparation for the magic...

Output (Python version(s)):

triggering_statement Some unexpected output

(Optional): One line describing the unexpected output.

💡 Explanation:

Set up code

More examples for further clarification (if necessary)

Output (Python version(s)):

trigger # some example that makes it easy to unveil the magic

some justified output

Note: All the examples are tested on Python 3.5.2 interactive interpreter, and they should work for all the Python versions unless explicitly specified before the output.

Usage

A nice way to get the most out of these examples, in my opinion, is to read them in sequential order, and for every example:


👀 Examples

Section: Strain your brain!

▶ First things first! *

For some reason, the Python 3.8's "Walrus" operator (:=) has become quite popular. Let's check it out,

Python version 3.8+

a = "wtf_walrus" a 'wtf_walrus'

a := "wtf_walrus" File "", line 1 a := "wtf_walrus" ^ SyntaxError: invalid syntax

(a := "wtf_walrus") # This works though 'wtf_walrus' a 'wtf_walrus'

2 .

Python version 3.8+

a = 6, 9 a (6, 9)

(a := 6, 9) (6, 9) a 6

a, b = 6, 9 # Typical unpacking a, b (6, 9) (a, b = 16, 19) # Oops File "", line 1 (a, b = 16, 19) ^ SyntaxError: invalid syntax

(a, b := 16, 19) # This prints out a weird 3-tuple (6, 16, 19)

a # a is still unchanged? 6

b 16

💡 Explanation

Quick walrus operator refresher

The Walrus operator (:=) was introduced in Python 3.8, it can be useful in situations where you'd want to assign values to variables within an expression.

def some_func(): # Assume some expensive computation here # time.sleep(1000) return 5

So instead of,

if some_func(): print(some_func()) # Which is bad practice since computation is happening twice

or

a = some_func() if a: print(a)

Now you can concisely write

if a := some_func(): print(a)

Output (> 3.8):

This saved one line of code, and implicitly prevented invoking some_func twice.


▶ Strings can be tricky sometimes

1. Notice that both the ids are same.

assert id("some_string") == id("some" + "_" + "string") assert id("some_string") == id("some_string")

2. True because it is invoked in script. Might be False in python shell or ipython

a = "wtf" b = "wtf" assert a is b

a = "wtf!" b = "wtf!" assert a is b

3. True because it is invoked in script. Might be False in python shell or ipython

a, b = "wtf!", "wtf!" assert a is b

a = "wtf!"; b = "wtf!" assert a is b

4. Disclaimer - snippet is not relavant in modern Python versions

Output (< Python3.7 )

'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' True 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' False

Makes sense, right?

💡 Explanation:

Shows a string interning process.


▶ Be careful with chained operations

(False == False) in [False] # makes sense False False == (False in [False]) # makes sense False False == False in [False] # now what? True

True is False == False False False is False is False True

1 > 0 < 1 True (1 > 0) < 1 False 1 > (0 < 1) False

💡 Explanation:

As per https://docs.python.org/3/reference/expressions.html#comparisons

Formally, if a, b, c, ..., y, z are expressions and op1, op2, ..., opN are comparison operators, then a op1 b op2 c ... y opN z is equivalent to a op1 b and b op2 c and ... y opN z, except that each expression is evaluated at most once.

While such behavior might seem silly to you in the above examples, it's fantastic with stuff like a == b == c and 0 <= x <= 100.


▶ How not to use is operator

The following is a very famous example present all over the internet.

a = 256 b = 256 a is b True

a = 257 b = 257 a is b False

a = [] b = [] a is b False

a = tuple() b = tuple() a is b True

3.Output

a, b = 257, 257 a is b True

Output (Python 3.7.x specifically)

a, b = 257, 257 a is b False

💡 Explanation:

The difference between is and ==

256 is an existing object but 257 isn't

When you start up python the numbers from -5 to 256 will be allocated. These numbers are used a lot, so it makes sense just to have them ready.

Quoting from https://docs.python.org/3/c-api/long.html

The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behavior of Python, in this case, is undefined. :-)

id(256) 10922528 a = 256 b = 256 id(a) 10922528 id(b) 10922528 id(257) 140084850247312 x = 257 y = 257 id(x) 140084850247440 id(y) 140084850247344

Here the interpreter isn't smart enough while executing y = 257 to recognize that we've already created an integer of the value 257, and so it goes on to create another object in the memory.

Similar optimization applies to other immutable objects like empty tuples as well. Since lists are mutable, that's why [] is [] will return False and () is () will return True. This explains our second snippet. Let's move on to the third one,

Both a and b refer to the same object when initialized with same value in the same line.

Output

a, b = 257, 257 id(a) 140640774013296 id(b) 140640774013296 a = 257 b = 257 id(a) 140640774013392 id(b) 140640774013488


▶ Hash brownies

some_dict = {} some_dict[5.5] = "JavaScript" some_dict[5.0] = "Ruby" some_dict[5] = "Python"

Output:

some_dict[5.5] "JavaScript" some_dict[5.0] # "Python" destroyed the existence of "Ruby"? "Python" some_dict[5] "Python"

complex_five = 5 + 0j type(complex_five) complex some_dict[complex_five] "Python"

So, why is Python all over the place?

💡 Explanation


▶ Deep down, we're all the same.

Output:

WTF() == WTF() # two different instances can't be equal False WTF() is WTF() # identities are also different False hash(WTF()) == hash(WTF()) # hashes should be different as well True id(WTF()) == id(WTF()) True

💡 Explanation:


▶ Disorder within order *

from collections import OrderedDict

dictionary = dict() dictionary[1] = 'a'; dictionary[2] = 'b';

ordered_dict = OrderedDict() ordered_dict[1] = 'a'; ordered_dict[2] = 'b';

another_ordered_dict = OrderedDict() another_ordered_dict[2] = 'b'; another_ordered_dict[1] = 'a';

class DictWithHash(dict): """ A dict that also implements hash magic. """ hash = lambda self: 0

class OrderedDictWithHash(OrderedDict): """ An OrderedDict that also implements hash magic. """ hash = lambda self: 0

Output

dictionary == ordered_dict # If a == b True dictionary == another_ordered_dict # and b == c True ordered_dict == another_ordered_dict # then why isn't c == a ?? False

We all know that a set consists of only unique elements,

let's try making a set of these dictionaries and see what happens...

len({dictionary, ordered_dict, another_ordered_dict}) Traceback (most recent call last): File "", line 1, in TypeError: unhashable type: 'dict'

Makes sense since dict don't have hash implemented, let's use

our wrapper classes.

dictionary = DictWithHash() dictionary[1] = 'a'; dictionary[2] = 'b'; ordered_dict = OrderedDictWithHash() ordered_dict[1] = 'a'; ordered_dict[2] = 'b'; another_ordered_dict = OrderedDictWithHash() another_ordered_dict[2] = 'b'; another_ordered_dict[1] = 'a'; len({dictionary, ordered_dict, another_ordered_dict}) 1 len({ordered_dict, another_ordered_dict, dictionary}) # changing the order 2

What is going on here?

💡 Explanation:


▶ Keep trying... *

def some_func(): try: return 'from_try' finally: return 'from_finally'

def another_func(): for _ in range(3): try: continue finally: print("Finally!")

def one_more_func(): # A gotcha! try: for i in range(3): try: 1 / i except ZeroDivisionError: # Let's throw it here and handle it outside for loop raise ZeroDivisionError("A trivial divide by zero error") finally: print("Iteration", i) break except ZeroDivisionError as e: print("Zero division error occurred", e)

Output:

some_func() 'from_finally'

another_func() Finally! Finally! Finally!

1 / 0 Traceback (most recent call last): File "", line 1, in ZeroDivisionError: division by zero

one_more_func() Iteration 0

💡 Explanation:


▶ For what?

some_string = "wtf" some_dict = {} for i, some_dict[i] in enumerate(some_string): i = 10

Output:

some_dict # An indexed dict appears. {0: 'w', 1: 't', 2: 'f'}

💡 Explanation:

for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]  

Where exprlist is the assignment target. This means that the equivalent of {exprlist} = {next_value} is executed for each item in the iterable. An interesting example that illustrates this:
for i in range(4):
print(i)
i = 10
Output:
Did you expect the loop to run just once?
💡 Explanation:


▶ Evaluation time discrepancy

array = [1, 8, 15]

A typical generator expression

gen = (x for x in array if array.count(x) > 0) array = [2, 8, 22]

Output:

print(list(gen)) # Where did the other values go? [8]

array_1 = [1,2,3,4] gen_1 = (x for x in array_1) array_1 = [1,2,3,4,5]

array_2 = [1,2,3,4] gen_2 = (x for x in array_2) array_2[:] = [1,2,3,4,5]

Output:

print(list(gen_1)) [1, 2, 3, 4]

print(list(gen_2)) [1, 2, 3, 4, 5]

array_3 = [1, 2, 3] array_4 = [10, 20, 30] gen = (i + j for i in array_3 for j in array_4)

array_3 = [4, 5, 6] array_4 = [400, 500, 600]

Output:

print(list(gen)) [401, 501, 601, 402, 502, 602, 403, 503, 603]

💡 Explanation


is not ... is not is (not ...)

'something' is not None True 'something' is (not None) False

💡 Explanation


▶ A tic-tac-toe where X wins in the first attempt!

Let's initialize a row

row = [""] * 3 #row i['', '', '']

Let's make a board

board = [row] * 3

Output:

board [['', '', ''], ['', '', ''], ['', '', '']] board[0] ['', '', ''] board[0][0] '' board[0][0] = "X" board [['X', '', ''], ['X', '', ''], ['X', '', '']]

We didn't assign three "X"s, did we?

💡 Explanation:

When we initialize row variable, this visualization explains what happens in the memory

Shows a memory segment after row is initialized.

And when the board is initialized by multiplying the row, this is what happens inside the memory (each of the elements board[0], board[1] and board[2] is a reference to the same list referred by row)

Shows a memory segment after board is initialized.

We can avoid this scenario here by not using row variable to generate board. (Asked in this issue).

board = [['']*3 for _ in range(3)] board[0][0] = "X" board [['X', '', ''], ['', '', ''], ['', '', '']]


▶ Schrödinger's variable *

funcs = [] results = [] for x in range(7): def some_func(): return x funcs.append(some_func) results.append(some_func()) # note the function call here

funcs_results = [func() for func in funcs]

Output (Python version):

results [0, 1, 2, 3, 4, 5, 6] funcs_results [6, 6, 6, 6, 6, 6, 6]

The values of x were different in every iteration prior to appending some_func to funcs, but all the functions return 6 when they're evaluated after the loop completes.

powers_of_x = [lambda x: x**i for i in range(10)] [f(2) for f in powers_of_x] [512, 512, 512, 512, 512, 512, 512, 512, 512, 512]

💡 Explanation:

import inspect inspect.getclosurevars(funcs[0]) ClosureVars(nonlocals={}, globals={'x': 6}, builtins={}, unbound=set())

Since x is a global value, we can change the value that the funcs will lookup and return by updating x:

x = 42 [func() for func in funcs] [42, 42, 42, 42, 42, 42, 42]

funcs = [] for x in range(7): def some_func(x=x): return x funcs.append(some_func)

Output:

funcs_results = [func() for func in funcs] funcs_results [0, 1, 2, 3, 4, 5, 6]

It is not longer using the x in the global scope:

inspect.getclosurevars(funcs[0]) ClosureVars(nonlocals={}, globals={}, builtins={}, unbound=set())


▶ The chicken-egg problem *

isinstance(3, int) True isinstance(type, object) True isinstance(object, type) True

So which is the "ultimate" base class? There's more to the confusion by the way,

class A: pass isinstance(A, A) False isinstance(type, type) True isinstance(object, object) True

issubclass(int, object) True issubclass(type, object) True issubclass(object, type) False

💡 Explanation


▶ Subclass relationships

Output:

from collections.abc import Hashable issubclass(list, object) True issubclass(object, Hashable) True issubclass(list, Hashable) False

The Subclass relationships were expected to be transitive, right? (i.e., if A is a subclass of B, and B is a subclass of C, the A should a subclass of C)

💡 Explanation:


▶ Methods equality and identity

class SomeClass: def method(self): pass

@classmethod
def classm(cls):
    pass

@staticmethod
def staticm():
    pass

Output:

print(SomeClass.method is SomeClass.method) True print(SomeClass.classm is SomeClass.classm) False print(SomeClass.classm == SomeClass.classm) True print(SomeClass.staticm is SomeClass.staticm) True

Accessing classm twice, we get an equal object, but not the same one? Let's see what happens with instances of SomeClass:

o1 = SomeClass() o2 = SomeClass()

Output:

print(o1.method == o2.method) False print(o1.method == o1.method) True print(o1.method is o1.method) False print(o1.classm is o1.classm) False print(o1.classm == o1.classm == o2.classm == SomeClass.classm) True print(o1.staticm is o1.staticm is o2.staticm is SomeClass.staticm) True

Accessing classm or method twice, creates equal but not same objects for the same instance of SomeClass.

💡 Explanation

o1.method <bound method SomeClass.method of <__main__.SomeClass object at ...>>

SomeClass.method <function SomeClass.method at ...>

o1.classm <bound method SomeClass.classm of <class '__main__.SomeClass'>>

SomeClass.classm <bound method SomeClass.classm of <class '__main__.SomeClass'>>

o1.staticm <function SomeClass.staticm at ...> SomeClass.staticm <function SomeClass.staticm at ...>

▶ All-true-ation *

all([True, True, True]) True all([True, True, False]) False

all([]) True all([[]]) False all([[[]]]) True

Why's this True-False alteration?

💡 Explanation:


▶ The surprising comma

Output (< 3.6):

def f(x, y,): ... print(x, y) ... def g(x=4, y=5,): ... print(x, y) ... def h(x, **kwargs,): File "", line 1 def h(x, **kwargs,): ^ SyntaxError: invalid syntax

def h(*args,): File "", line 1 def h(*args,): ^ SyntaxError: invalid syntax

💡 Explanation:


▶ Strings and the backslashes

Output:

print(""") "

print(r""") "

print(r"") File "", line 1 print(r"") ^ SyntaxError: EOL while scanning string literal

r''' == "\'" True

💡 Explanation


▶ not knot!

Output:

not x == y True x == not y File "", line 1 x == not y ^ SyntaxError: invalid syntax

💡 Explanation:


▶ Half triple-quoted strings

Output:

print('wtfpython''') wtfpython print("wtfpython""") wtfpython

The following statements raise SyntaxError

print('''wtfpython')

print("""wtfpython")

File "", line 3 print("""wtfpython") ^ SyntaxError: EOF while scanning triple-quoted string literal

💡 Explanation:

>>> print("wtf" "python")  
wtfpython  
>>> print("wtf" "") # or "wtf"""  
wtf  

▶ What's wrong with booleans?

A simple example to count the number of booleans and

integers in an iterable of mixed data types.

mixed_list = [False, 1.0, "some_string", 3, True, [], False] integers_found_so_far = 0 booleans_found_so_far = 0

for item in mixed_list: if isinstance(item, int): integers_found_so_far += 1 elif isinstance(item, bool): booleans_found_so_far += 1

Output:

integers_found_so_far 4 booleans_found_so_far 0

some_bool = True "wtf" * some_bool 'wtf' some_bool = False "wtf" * some_bool ''

def tell_truth(): True = False if True == False: print("I have lost faith in truth!")

Output (< 3.x):

tell_truth() I have lost faith in truth!

💡 Explanation:


▶ Class attributes and instance attributes

class A: x = 1

class B(A): pass

class C(A): pass

Output:

A.x, B.x, C.x (1, 1, 1) B.x = 2 A.x, B.x, C.x (1, 2, 1) A.x = 3 A.x, B.x, C.x # C.x changed, but B.x didn't (3, 2, 3) a = A() a.x, A.x (3, 3) a.x += 1 a.x, A.x (4, 3)

class SomeClass: some_var = 15 some_list = [5] another_list = [5] def init(self, x): self.some_var = x + 1 self.some_list = self.some_list + [x] self.another_list += [x]

Output:

some_obj = SomeClass(420) some_obj.some_list [5, 420] some_obj.another_list [5, 420] another_obj = SomeClass(111) another_obj.some_list [5, 111] another_obj.another_list [5, 420, 111] another_obj.another_list is SomeClass.another_list True another_obj.another_list is some_obj.another_list True

💡 Explanation:


▶ yielding None

some_iterable = ('a', 'b')

def some_func(val): return "something"

Output (<= 3.7.x):

[x for x in some_iterable] ['a', 'b'] [(yield x) for x in some_iterable] <generator object at 0x7f70b0a4ad58> list([(yield x) for x in some_iterable]) ['a', 'b'] list((yield x) for x in some_iterable) ['a', None, 'b', None] list(some_func((yield x)) for x in some_iterable) ['a', 'something', 'b', 'something']

💡 Explanation:


▶ Yielding from... return! *

def some_func(x): if x == 3: return ["wtf"] else: yield from range(x)

Output (> 3.3):

list(some_func(3)) []

Where did the "wtf" go? Is it due to some special effect of yield from? Let's validate that,

def some_func(x): if x == 3: return ["wtf"] else: for i in range(x): yield i

Output:

list(some_func(3)) []

The same result, this didn't work either.

💡 Explanation:

"... return expr in a generator causes StopIteration(expr) to be raised upon exit from the generator."


▶ Nan-reflexivity *

a = float('inf') b = float('nan') c = float('-iNf') # These strings are case-insensitive d = float('nan')

Output:

a inf b nan c -inf float('some_other_string') ValueError: could not convert string to float: some_other_string a == -c # inf==inf True None == None # None == None True b == d # but nan!=nan False 50 / a 0.0 a / a nan 23 + b nan

x = float('nan') y = x / x y is y # identity holds True y == y # equality fails of y False [y] == [y] # but the equality succeeds for the list containing y True

💡 Explanation:


▶ Mutating the immutable!

This might seem trivial if you know how references work in Python.

some_tuple = ("A", "tuple", "with", "values") another_tuple = ([1, 2], [3, 4], [5, 6])

Output:

some_tuple[2] = "change this" TypeError: 'tuple' object does not support item assignment another_tuple[2].append(1000) #This throws no error another_tuple ([1, 2], [3, 4], [5, 6, 1000]) another_tuple[2] += [99, 999] TypeError: 'tuple' object does not support item assignment another_tuple ([1, 2], [3, 4], [5, 6, 1000, 99, 999])

But I thought tuples were immutable...

💡 Explanation:


▶ The disappearing variable from outer scope

e = 7 try: raise Exception() except Exception as e: pass

Output (Python 2.x):

print(e)

prints nothing

Output (Python 3.x):

print(e) NameError: name 'e' is not defined

💡 Explanation:

This means the exception must be assigned to a different name to be able to refer to it after the except clause. Exceptions are cleared because, with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs.

Nothing is printed!


▶ The mysterious key type conversion

class SomeClass(str): pass

some_dict = {'s': 42}

Output:

type(list(some_dict.keys())[0]) str s = SomeClass('s') some_dict[s] = 40 some_dict # expected: Two different keys-value pairs {'s': 40} type(list(some_dict.keys())[0]) str

💡 Explanation:


▶ Let's see if you can guess this?

Output:

💡 Explanation:

(target_list "=")+ (expression_list | yield_expression)  

and

An assignment statement evaluates the expression list (remember that this can be a single expression or a comma-separated list, the latter yielding a tuple) and assigns the single resulting object to each of the target lists, from left to right.


▶ Exceeds the limit for integer string conversion

Python 3.10.6

int("2" * 5432)

Python 3.10.8

int("2" * 5432)

Output:

Python 3.10.6

222222222222222222222222222222222222222222222222222222222222222...

Python 3.10.8

Traceback (most recent call last): ... ValueError: Exceeds the limit (4300) for integer string conversion: value has 5432 digits; use sys.set_int_max_str_digits() to increase the limit.

💡 Explanation:

This call to int() works fine in Python 3.10.6 and raises a ValueError in Python 3.10.8. Note that Python can still work with large integers. The error is only raised when converting between integers and strings.

Fortunately, you can increase the limit for the allowed number of digits when you expect an operation to exceed it. To do this, you can use one of the following:

Check the documentation for more details on changing the default limit if you expect your code to exceed this value.


Section: Slippery Slopes

▶ Modifying a dictionary while iterating over it

x = {0: None}

for i in x: del x[i] x[i+1] = None print(i)

Output (Python 2.7- Python 3.5):

Yes, it runs for exactly eight times and stops.

💡 Explanation:


▶ Stubborn del operation

class SomeClass: def del(self): print("Deleted!")

**Output:**1.

x = SomeClass() y = x del x # this should print "Deleted!" del y Deleted!

Phew, deleted at last. You might have guessed what saved __del__ from being called in our first attempt to delete x. Let's add more twists to the example.

x = SomeClass() y = x del x y # check if y exists <__main__.SomeClass instance at 0x7f98a1a67fc8> del y # Like previously, this should print "Deleted!" globals() # oh, it didn't. Let's check all our global variables and confirm Deleted! {'builtins': <module '__builtin__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, 'package': None, 'name': 'main', 'doc': None}

Okay, now it's deleted 😕

💡 Explanation:


▶ The out of scope variable

a = 1 def some_func(): return a

def another_func(): a += 1 return a

def some_closure_func(): a = 1 def some_inner_func(): return a return some_inner_func()

def another_closure_func(): a = 1 def another_inner_func(): a += 1 return a return another_inner_func()

Output:

some_func() 1 another_func() UnboundLocalError: local variable 'a' referenced before assignment

some_closure_func() 1 another_closure_func() UnboundLocalError: local variable 'a' referenced before assignment

💡 Explanation:


▶ Deleting a list item while iterating

list_1 = [1, 2, 3, 4] list_2 = [1, 2, 3, 4] list_3 = [1, 2, 3, 4] list_4 = [1, 2, 3, 4]

for idx, item in enumerate(list_1): del item

for idx, item in enumerate(list_2): list_2.remove(item)

for idx, item in enumerate(list_3[:]): list_3.remove(item)

for idx, item in enumerate(list_4): list_4.pop(idx)

Output:

list_1 [1, 2, 3, 4] list_2 [2, 4] list_3 [] list_4 [2, 4]

Can you guess why the output is [2, 4]?

💡 Explanation:

Difference between del, remove, and pop:

Why the output is [2, 4]?


▶ Lossy zip of iterators *

numbers = list(range(7)) numbers [0, 1, 2, 3, 4, 5, 6] first_three, remaining = numbers[:3], numbers[3:] first_three, remaining ([0, 1, 2], [3, 4, 5, 6]) numbers_iter = iter(numbers) list(zip(numbers_iter, first_three)) [(0, 0), (1, 1), (2, 2)]

so far so good, let's zip the remaining

list(zip(numbers_iter, remaining)) [(4, 3), (5, 4), (6, 5)]

Where did element 3 go from the numbers list?

💡 Explanation:


▶ Loop variables leaking out!

for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global')

Output:

6 : for x inside loop 6 : x in global

But x was never defined outside the scope of for loop...

This time let's initialize x first

x = -1 for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global')

Output:

6 : for x inside loop 6 : x in global

Output (Python 2.x):

x = 1 print([x for x in range(5)]) [0, 1, 2, 3, 4] print(x) 4

Output (Python 3.x):

x = 1 print([x for x in range(5)]) [0, 1, 2, 3, 4] print(x) 1

💡 Explanation:


▶ Beware of default mutable arguments!

def some_func(default_arg=[]): default_arg.append("some_string") return default_arg

Output:

some_func() ['some_string'] some_func() ['some_string', 'some_string'] some_func([]) ['some_string'] some_func() ['some_string', 'some_string', 'some_string']

💡 Explanation:


▶ Catching the Exceptions

some_list = [1, 2, 3] try: # This should raise an IndexError print(some_list[4]) except IndexError, ValueError: print("Caught!")

try: # This should raise a ValueError some_list.remove(4) except IndexError, ValueError: print("Caught again!")

Output (Python 2.x):

Caught!

ValueError: list.remove(x): x not in list

Output (Python 3.x):

File "", line 3 except IndexError, ValueError: ^ SyntaxError: invalid syntax

💡 Explanation

Caught again!  
list.remove(x): x not in list  

Output (Python 3.x):
File "", line 4
except (IndexError, ValueError), e:
^
IndentationError: unindent does not match any outer indentation level

Caught again!  
list.remove(x): x not in list  

▶ Same operands, different story!

a = [1, 2, 3, 4] b = a a = a + [5, 6, 7, 8]

Output:

a [1, 2, 3, 4, 5, 6, 7, 8] b [1, 2, 3, 4]

a = [1, 2, 3, 4] b = a a += [5, 6, 7, 8]

Output:

a [1, 2, 3, 4, 5, 6, 7, 8] b [1, 2, 3, 4, 5, 6, 7, 8]

💡 Explanation:


▶ Name resolution ignoring class scope

x = 5 class SomeClass: x = 17 y = (x for i in range(10))

Output:

list(SomeClass.y)[0] 5

x = 5 class SomeClass: x = 17 y = [x for i in range(10)]

Output (Python 2.x):

Output (Python 3.x):

💡 Explanation


▶ Rounding like a banker *

Let's implement a naive function to get the middle element of a list:

def get_middle(some_list): mid_index = round(len(some_list) / 2) return some_list[mid_index - 1]

Python 3.x:

get_middle([1]) # looks good 1 get_middle([1,2,3]) # looks good 2 get_middle([1,2,3,4,5]) # huh? 2 len([1,2,3,4,5]) / 2 # good 2.5 round(len([1,2,3,4,5]) / 2) # why? 2

It seems as though Python rounded 2.5 to 2.

💡 Explanation:

round(0.5) 0 round(1.5) 2 round(2.5) 2 import numpy # numpy does the same numpy.round(0.5) 0.0 numpy.round(1.5) 2.0 numpy.round(2.5) 2.0


▶ Needles in a Haystack *

I haven't met even a single experience Pythonist till date who has not come across one or more of the following scenarios,

x, y = (0, 1) if True else None, None

Output:

x, y # expected (0, 1) ((0, 1), None)

t = ('one', 'two') for i in t: print(i)

t = ('one') for i in t: print(i)

t = () print(t)

Output:

ten_words_list = [
    "some",
    "very",
    "big",
    "list",
    "that"
    "consists",
    "of",
    "exactly",
    "ten",
    "words"
]

Output

len(ten_words_list) 9

4. Not asserting strongly enough

a = "python" b = "javascript"

Output:

An assert statement with an assertion failure message.

assert(a == b, "Both languages are different")

No AssertionError is raised

some_list = [1, 2, 3] some_dict = { "key_1": 1, "key_2": 2, "key_3": 3 }

some_list = some_list.append(4) some_dict = some_dict.update({"key_4": 4})

Output:

print(some_list) None print(some_dict) None

def some_recursive_func(a): if a[0] == 0: return a[0] -= 1 some_recursive_func(a) return a

def similar_recursive_func(a): if a == 0: return a a -= 1 similar_recursive_func(a) return a

Output:

some_recursive_func([5, 0]) [0, 0] similar_recursive_func(5) 4

💡 Explanation:


▶ Splitsies *

'a'.split() ['a']

is same as

'a'.split(' ') ['a']

but

len(''.split()) 0

isn't the same as

len(''.split(' ')) 1

💡 Explanation:


▶ Wild imports *

File: module.py

def some_weird_name_func_(): print("works!")

def _another_weird_name_func(): print("works!")

Output

from module import * some_weird_name_func_() "works!" _another_weird_name_func() Traceback (most recent call last): File "", line 1, in NameError: name '_another_weird_name_func' is not defined

💡 Explanation:


▶ All sorted? *

x = 7, 8, 9 sorted(x) == x False sorted(x) == sorted(x) True

y = reversed(x) sorted(y) == sorted(y) False

💡 Explanation:


▶ Midnight time doesn't exist?

from datetime import datetime

midnight = datetime(2018, 1, 1, 0, 0) midnight_time = midnight.time()

noon = datetime(2018, 1, 1, 12, 0) noon_time = noon.time()

if midnight_time: print("Time at midnight is", midnight_time)

if noon_time: print("Time at noon is", noon_time)

Output (< 3.5):

('Time at noon is', datetime.time(12, 0))

The midnight time is not printed.

💡 Explanation:

Before Python 3.5, the boolean value for datetime.time object was considered to be False if it represented midnight in UTC. It is error-prone when using the if obj: syntax to check if the obj is null or some equivalent of "empty."



Section: The Hidden treasures!

This section contains a few lesser-known and interesting things about Python that most beginners like me are unaware of (well, not anymore).

▶ Okay Python, Can you make me fly?

Well, here you go

**Output:**Sshh... It's a super-secret.

💡 Explanation:


goto, but why?

from goto import goto, label for i in range(9): for j in range(9): for k in range(9): print("I am trapped, please rescue!") if k == 2: goto .breakout # breaking out from a deeply nested loop label .breakout print("Freedom!")

Output (Python 2.3):

I am trapped, please rescue! I am trapped, please rescue! Freedom!

💡 Explanation:


▶ Brace yourself!

If you are one of the people who doesn't like using whitespace in Python to denote scopes, you can use the C-style {} by importing,

from future import braces

Output:

File "some_file.py", line 1 from future import braces SyntaxError: not a chance

Braces? No way! If you think that's disappointing, use Java. Okay, another surprising thing, can you find where's the SyntaxError raised in __future__ module code?

💡 Explanation:


▶ Let's meet Friendly Language Uncle For Life

Output (Python 3.x)

from future import barry_as_FLUFL "Ruby" != "Python" # there's no doubt about it File "some_file.py", line 1 "Ruby" != "Python" ^ SyntaxError: invalid syntax

"Ruby" <> "Python" True

There we go.

💡 Explanation:


▶ Even Python understands that love is complicated

Wait, what's this? this is love ❤️

Output:

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

It's the Zen of Python!

love = this this is love True love is True False love is False False love is not True or False True love is not True or False; love is love # Love is complicated True

💡 Explanation:


▶ Yes, it exists!

The else clause for loops. One typical example might be:

def does_exists_num(l, to_find): for num in l: if num == to_find: print("Exists!") break else: print("Does not exist")

Output:

some_list = [1, 2, 3, 4, 5] does_exists_num(some_list, 4) Exists! does_exists_num(some_list, -1) Does not exist

The else clause in exception handling. An example,

try: pass except: print("Exception occurred!!!") else: print("Try block executed successfully...")

Output:

Try block executed successfully...

💡 Explanation:


▶ Ellipsis *

def some_func(): Ellipsis

Output

some_func()

No output, No Error

SomeRandomString Traceback (most recent call last): File "", line 1, in NameError: name 'SomeRandomString' is not defined

Ellipsis Ellipsis

💡 Explanation


▶ Inpinity

The spelling is intended. Please, don't submit a patch for this.

Output (Python 3.x):

infinity = float('infinity') hash(infinity) 314159 hash(float('-inf')) -314159

💡 Explanation:


▶ Let's mangle

class Yo(object): def init(self): self.__honey = True self.bro = True

Output:

Yo().bro True Yo().__honey AttributeError: 'Yo' object has no attribute '__honey' Yo()._Yo__honey True

class Yo(object): def init(self): # Let's try something symmetrical this time self.honey = True self.bro = True

Output:

Yo().bro True

Yo().Yo__honey_ Traceback (most recent call last): File "", line 1, in AttributeError: 'Yo' object has no attribute 'Yo__honey_'

Why did Yo()._Yo__honey work?

_A__variable = "Some value"

class A(object): def some_func(self): return __variable # not initialized anywhere yet

Output:

A().__variable Traceback (most recent call last): File "", line 1, in AttributeError: 'A' object has no attribute '__variable'

A().some_func() 'Some value'

💡 Explanation:



Section: Appearances are deceptive!

▶ Skipping lines?

Output:

value = 11 valuе = 32 value 11

Wut?

Note: The easiest way to reproduce this is to simply copy the statements from the above snippet and paste them into your file/shell.

💡 Explanation

Some non-Western characters look identical to letters in the English alphabet but are considered distinct by the interpreter.

ord('е') # cyrillic 'e' (Ye) 1077 ord('e') # latin 'e', as used in English and typed using standard keyboard 101 'е' == 'e' False

value = 42 # latin e valuе = 23 # cyrillic 'e', Python 2.x interpreter would raise a SyntaxError here value 42

The built-in ord() function returns a character's Unicode code point, and different code positions of Cyrillic 'e' and Latin 'e' justify the behavior of the above example.


▶ Teleportation

pip install numpy first.

import numpy as np

def energy_send(x): # Initializing a numpy array np.array([float(x)])

def energy_receive(): # Return an empty numpy array return np.empty((), dtype=np.float).tolist()

Output:

energy_send(123.456) energy_receive() 123.456

Where's the Nobel Prize?

💡 Explanation:


▶ Well, something is fishy...

def square(x): """ A simple function to calculate the square of a number by addition. """ sum_so_far = 0 for counter in range(x): sum_so_far = sum_so_far + x return sum_so_far

Output (Python 2.x):

Shouldn't that be 100?

Note: If you're not able to reproduce this, try running the file mixed_tabs_and_spaces.py via the shell.

💡 Explanation



Section: Miscellaneous

+= is faster

using "+", three strings:

timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 0.25748300552368164

using "+=", three strings:

timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 0.012188911437988281

💡 Explanation:


▶ Let's make a giant string!

def add_string_with_plus(iters): s = "" for i in range(iters): s += "xyz" assert len(s) == 3*iters

def add_bytes_with_plus(iters): s = b"" for i in range(iters): s += b"xyz" assert len(s) == 3*iters

def add_string_with_format(iters): fs = "{}"iters s = fs.format((["xyz"]iters)) assert len(s) == 3iters

def add_string_with_join(iters): l = [] for i in range(iters): l.append("xyz") s = "".join(l) assert len(s) == 3*iters

def convert_list_to_string(l, iters): s = "".join(l) assert len(s) == 3*iters

Output:

Executed in ipython shell using %timeit for better readability of results.

You can also use the timeit module in normal python shell/scriptm=, example usage below

timeit.timeit('add_string_with_plus(10000)', number=1000, globals=globals())

NUM_ITERS = 1000 %timeit -n1000 add_string_with_plus(NUM_ITERS) 124 µs ± 4.73 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) %timeit -n1000 add_bytes_with_plus(NUM_ITERS) 211 µs ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) %timeit -n1000 add_string_with_format(NUM_ITERS) 61 µs ± 2.18 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) %timeit -n1000 add_string_with_join(NUM_ITERS) 117 µs ± 3.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) l = ["xyz"]*NUM_ITERS %timeit -n1000 convert_list_to_string(l, NUM_ITERS) 10.1 µs ± 1.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Let's increase the number of iterations by a factor of 10.

NUM_ITERS = 10000 %timeit -n1000 add_string_with_plus(NUM_ITERS) # Linear increase in execution time 1.26 ms ± 76.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) %timeit -n1000 add_bytes_with_plus(NUM_ITERS) # Quadratic increase 6.82 ms ± 134 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) %timeit -n1000 add_string_with_format(NUM_ITERS) # Linear increase 645 µs ± 24.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) %timeit -n1000 add_string_with_join(NUM_ITERS) # Linear increase 1.17 ms ± 7.25 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) l = ["xyz"]*NUM_ITERS %timeit -n1000 convert_list_to_string(l, NUM_ITERS) # Linear increase 86.3 µs ± 2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

💡 Explanation


▶ Slowing down dict lookups *

some_dict = {str(i): 1 for i in range(1_000_000)} another_dict = {str(i): 1 for i in range(1_000_000)}

Output:

%timeit some_dict['5'] 28.6 ns ± 0.115 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) some_dict[1] = 1 %timeit some_dict['5'] 37.2 ns ± 0.265 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

%timeit another_dict['5'] 28.5 ns ± 0.142 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) another_dict[1] # Trying to access a key that doesn't exist Traceback (most recent call last): File "", line 1, in KeyError: 1 %timeit another_dict['5'] 38.5 ns ± 0.0913 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Why are same lookups becoming slower?

💡 Explanation:

▶ Bloating instance dicts *

import sys

class SomeClass: def init(self): self.some_attr1 = 1 self.some_attr2 = 2 self.some_attr3 = 3 self.some_attr4 = 4

def dict_size(o): return sys.getsizeof(o.dict)

Output: (Python 3.8, other Python 3 versions may vary a little)

o1 = SomeClass() o2 = SomeClass() dict_size(o1) 104 dict_size(o2) 104 del o1.some_attr1 o3 = SomeClass() dict_size(o3) 232 dict_size(o1) 232

Let's try again... In a new interpreter:

o1 = SomeClass() o2 = SomeClass() dict_size(o1) 104 # as expected o1.some_attr5 = 5 o1.some_attr6 = 6 dict_size(o1) 360 dict_size(o2) 272 o3 = SomeClass() dict_size(o3) 232

What makes those dictionaries become bloated? And why are newly created objects bloated as well?

💡 Explanation:

▶ Minor Ones *

File some_file.py

import time
print("wtfpython", end="_")
time.sleep(3)
This will print the wtfpython after 3 seconds due to the end argument because the output buffer is flushed either after encountering \n or when the program finishes execution. We can force the buffer to flush by passing flush=True argument.



Contributing

A few ways in which you can contribute to wtfpython,

Please see CONTRIBUTING.md for more details. Feel free to create a new issue to discuss things.

PS: Please don't reach out with backlinking requests, no links will be added unless they're highly relevant to the project.

Acknowledgements

The idea and design for this collection were initially inspired by Denys Dovhan's awesome project wtfjs. The overwhelming support by Pythonistas gave it the shape it is in right now.

🎓 License

WTFPL 2.0

© Satwik Kansal

Surprise your friends as well!

If you like wtfpython, you can use these quick links to share it with your friends,

Twitter | Linkedin | Facebook

Need a pdf version?

I've received a few requests for the pdf (and epub) version of wtfpython. You can add your details here to get them as soon as they are finished.

That's all folks! For upcoming content like this, you can add your email here.