We recently wrote about funny, cunning, and weird JavaScript examples. Now it's Python's turn. Python, a high-level and interpreted language, has many convenient features. But sometimes the result of the work of some pieces of code at first glance looks unobvious.
Below is a fun project that collects examples of unexpected behavior in Python with a discussion of what is happening under the hood. Some examples do not fall into the category of real WTF?!, But instead they demonstrate interesting features of the language that you may want to avoid. I think this is a good way to learn the internal workings of Python, and I hope you find it interesting.
If you are already an experienced programmer in Python, then many examples may be familiar to you, and even cause nostalgia for those times when you racked your brains over them :)
is not what it isis not ... different from is (not ...)Return returns everywhereNote: All examples given are tested on the Python 3.5.2 interactive interpreter and should work in all versions of the language, unless otherwise explicitly stated in the description.
Structure of examples:
# . # ... Result (Python version):
>>> _ , (Optional): A one-line description of the unexpected result.
Explanation:
A brief explanation of what happened and why.
( ) Result:
>>> # - , # It seems to me that the best way to get the most out of these examples is to read them in chronological order:
PS You can also read these examples on the command line. Only first install the wtfpython npm package,
$ npm install -g wtfpython Now run wtfpython on the command line, and as a result, this collection will open in your $PAGER .
#TODO: Add the pypi package for reading at the command line.
Result:
>>> value = 11 >>> valu = 32 >>> value 11 Wat?
Note: The easiest way to reproduce this example is by copying and pasting into your file / shell.
Explanation
Some Unicode characters look the same as ASCII, but differ in interpreter.
>>> value = 42 #ascii e >>> valu = 23 #cyrillic e, Python 2.x interpreter would raise a `SyntaxError` here >>> value 42 def square(x): """ . """ sum_so_far = 0 for counter in range(x): sum_so_far = sum_so_far + x return sum_so_far Result (Python 2.x):
>>> square(10) 10 Shouldn't it have turned 100?
Note: if you cannot reproduce the result, try running the file mixed_tabs_and_spaces.py in the shell.
Explanation
square is replaced by eight spaces and falls into a loop.Result (Python 3.x):
TabError: inconsistent use of tabs and spaces in indentation one.
some_dict = {} some_dict[5.5] = "Ruby" some_dict[5.0] = "JavaScript" some_dict[5] = "Python" Result:
>>> some_dict[5.5] "Ruby" >>> some_dict[5.0] "Python" >>> some_dict[5] "Python" Python destroyed the existence of javascript?
Explanation
Immutable objects with the same values ​​in Python always get the same hashes.
>>> 5 == 5.0 True >>> hash(5) == hash(5.0) True Note: objects with different values ​​can also get the same hash (this situation is called a hash collision).
some_dict[5] = "Python" existing expression “JavaScript” is rewritten to “Python”, because Python recognizes 5 and 5.0 as identical keys of the dictionary some_dict . array = [1, 8, 15] g = (x for x in array if array.count(x) > 0) array = [2, 8, 22] Result:
>>> print(list(g)) [8] Explanation
in processed during the declaration, and the conditional clause is processed during the run time.array is reassigned to the list [2, 8, 22] , and since from 1 , 8 and 15 only the value of the counter 8 greater than 0 , the generator produces only 8 . x = {0: None} for i in x: del x[i] x[i+1] = None print(i) Result:
0 1 2 3 4 5 6 7 Yes, it runs exactly eight times and stops.
Explanation:
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) Result:
>>> list_1 [1, 2, 3, 4] >>> list_2 [2, 4] >>> list_3 [] >>> list_4 [2, 4] Do you know why the result was [2, 4] ?
Explanation:
Changing an object during its iteration is always a bad idea. It is better then to iterate a copy of the object, which list_3[:] does.
>>> some_list = [1, 2, 3, 4] >>> id(some_list) 139798789457608 >>> id(some_list[:]) # Notice that python creates new object for sliced list. 139798779601192 The difference between del , remove and pop :
del var_name simply removes the var_name binding of the local or global namespace (so list_1 remains unaffected).remove removes the first matching value, not the specific index, causing ValueError if there is no value.pop removes the item with a specific index and returns it, causing IndexError if an invalid index is specified.Why did it happen [2, 4] ?
1 from list_2 or list_4 , the contents of the lists become [2, 3, 4] . The rest are shifted down, that is, 2 turns on index 0, 3 - on index 1. Since the next iteration will be performed with reference to index 1 (where we have 3 ), 2 will be skipped. The same thing happens with every second item in the list. A similar example related to dictionaries in Python is beautifully explained on StackOverflow.Result:
>>> print("\\ some string \\") >>> print(r"\ some string") >>> print(r"\ some string \") File "<stdin>", line 1 print(r"\ some string \") ^ SyntaxError: EOL while scanning string literal Explanation
r , the backslash has no special meaning.This is not WTF, but only some cool things, and they need to be wary :)
def add_string_with_plus(iters): s = "" for i in range(iters): s += "xyz" assert len(s) == 3*iters def add_string_with_format(iters): fs = "{}"*iters s = fs.format(*(["xyz"]*iters)) assert len(s) == 3*iters 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 Result:
>>> timeit(add_string_with_plus(10000)) 100 loops, best of 3: 9.73 ms per loop >>> timeit(add_string_with_format(10000)) 100 loops, best of 3: 5.47 ms per loop >>> timeit(add_string_with_join(10000)) 100 loops, best of 3: 10.1 ms per loop >>> l = ["xyz"]*10000 >>> timeit(convert_list_to_string(l, 10000)) 10000 loops, best of 3: 75.3 µs per loop Explanation
+ to generate long strings: in Python, str is immutable, so for each pair of concatenations, the left and right strings must be copied into a new string. If you concatenate four lines 10 characters long, then copy (10 + 10) + ((10 + 10) + 10) + (((10 + 10) + 10) +10) = 90 characters instead of 40. As you grow the number and size of rows, the situation worsens fourfold..format. syntax .format. or % (but on short lines it works a little slower than +).''.join(iterable_object) . >>> a = "some_string" >>> id(a) 140420665652016 >>> id("some" + "_" + "string") # Notice that both the ids are same. 140420665652016 # 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:
+= faster + more than two lines, because the first line (for example, s1 for s1 += s2 + s3 ) is not destroyed until the line has been completely processed.Clause else for loops . Typical example:
def does_exists_num(l, to_find): for num in l: if num == to_find: print("Exists!") break else: print("Does not exist") Result:
>>> some_list = [1, 2, 3, 4, 5] >>> does_exists_num(some_list, 4) Exists!
>>> does_exists_num(some_list, -1) Does not exist.
Clause else in exception handling . Example:
try: pass except: print("Exception occurred!!!") else: print("Try block executed successfully...") Result:
Try block executed successfully... Explanation:
else is executed after the loop only when after all iterations there is no obvious break .else clause after a try block is also called a completion clause, since the availability of an else in a try means that the try block has been completed successfully.is not what it isThis example is very widely known.
>>> a = 256 >>> b = 256 >>> a is b True >>> a = 257 >>> b = 257 >>> a is b False >>> a = 257; b = 257 >>> a is b True Explanation:
Difference between is and ==
is operator checks that both operands refer to the same object (that is, it checks if they are identical to each other).== compares the values ​​of the operands and checks for identity.is used for equivalence of links, and == for equivalence of values. Explanatory example: >>> [] == [] True >>> [] is [] # These are two empty lists at two different memory locations. False 256 is an existing object, and 257 is not
When you start Python, numbers from -5 to 256 are placed in memory. They are used often, so it is advisable to keep them ready.
Quote from https://docs.python.org/3/c-api/long.html
The current implementation supports an array of integer objects for all numbers from –5 to 256, so that when you create an int from this range, you get a reference to an existing object. Therefore, it should be possible to change the value to 1. But I suspect that in this case the behavior of Python will be unpredictable. :-)
>>> 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 The interpreter was not so smart, and during execution y = 257 did not understand that we had already created an integer with the value 257 , therefore it creates another object in the memory.
a and b refer to the same object when initialized with the same value on the same line.
>>> a, b = 257, 257 >>> id(a) 140640774013296 >>> id(b) 140640774013296 >>> a = 257 >>> b = 257 >>> id(a) 140640774013392 >>> id(b) 140640774013488 a and b to 257 on one line, the Python interpreter creates a new object, while at the same time making a reference to it from the second variable. If we assign values ​​in different lines, the interpreter will not “know” that we already have 257 as an object..py file, you will not see this behavior, because the file is compiled at once.is not ... different from is (not ...) >>> 'something' is not None True >>> 'something' is (not None) False Explanation
is not is a single binary operator whose behavior differs from the situation when is and not are used separately.is not False if variables on both sides of the statement point to the same object. Otherwise, it is true. funcs = [] results = [] for x in range(7): def some_func(): return x funcs.append(some_func) results.append(some_func()) funcs_results = [func() for func in funcs] Result:
>>> results [0, 1, 2, 3, 4, 5, 6] >>> funcs_results [6, 6, 6, 6, 6, 6, 6] If before adding some_func in funcs , the x values ​​in each iteration were different, all functions returned 6.
//OR >>> 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
funcs = [] for x in range(7): def some_func(x=x): return x funcs.append(some_func) Result:
>>> funcs_results = [func() for func in funcs] >>> funcs_results [0, 1, 2, 3, 4, 5, 6] one.
for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global') Result:
6 : for x inside loop 6 : x in global But x not defined for a loop outside the scope.
2
# 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') Result:
6 : for x inside loop 6 : x in global 3
x = 1 print([x for x in range(5)]) print(x, ': x in global') Result (on Python 2.x):
[0, 1, 2, 3, 4] (4, ': x in global') Result (on Python 3.x):
[0, 1, 2, 3, 4] 1 : x in global Explanation
“The syntax form[... for var in item1, item2, ...]no longer supported for generating lists. Use[... for var in (item1, item2, ...)]instead. Also note that list generation has different semantics: they are closer to syntactic sugar in relation to the generating expression inside thelist()constructor, and, in particular, loop control variables no longer flow into the surrounding scope. ”
# Let's initialize a row row = [""]*3 #row i['', '', ''] # Let's make a board board = [row]*3 Result:
>>> board [['', '', ''], ['', '', ''], ['', '', '']] >>> board[0] ['', '', ''] >>> board[0][0] '' >>> board[0][0] = "X" >>> board [['X', '', ''], ['X', '', ''], ['X', '', '']] But we did not assign three X, right?
Explanation
This visualization explains what happens in memory when the row variable is initialized:

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

def some_func(default_arg=[]): default_arg.append("some_string") return default_arg Result:
>>> some_func() ['some_string'] >>> some_func() ['some_string', 'some_string'] >>> some_func([]) ['some_string'] >>> some_func() ['some_string', 'some_string', 'some_string'] Explanation
[] as an argument to some_func , there was no default value for the default_arg variable, so the function returned what was expected. def some_func(default_arg=[]): default_arg.append("some_string") return default_arg Result:
>>> some_func.__defaults__ #This will show the default argument values for the function ([],) >>> some_func() >>> some_func.__defaults__ (['some_string'],) >>> some_func() >>> some_func.__defaults__ (['some_string', 'some_string'],) >>> some_func([]) >>> some_func.__defaults__ (['some_string', 'some_string'],) None as the default value, followed by checking whether any value is passed to the function that corresponds to this argument. Example: def some_func(default_arg=None): if not default_arg: default_arg = [] default_arg.append("some_string") return default_arg one.
a = [1, 2, 3, 4] b = a a = a + [5, 6, 7, 8] Result:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4] 2
a = [1, 2, 3, 4] b = a a += [5, 6, 7, 8] Result:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4, 5, 6, 7, 8] Explanation
a += b behaves differently than a = a + ba = a + [5,6,7,8] generates a new object and assigns a to it, leaving b unchanged.a + =[5,6,7,8] actually converted (mapped to) into the extend function, which works with the object in such a way that a and b still point to the same object that was changed in place. some_tuple = ("A", "tuple", "with", "values") another_tuple = ([1, 2], [3, 4], [5, 6]) Result:
>>> 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 the tuples are immutable, is not it ...
Explanation
An immutable sequence type object cannot change after its creation. If an object contains references to other objects, then these objects can be editable and can be changed. However, a collection of objects that is directly referenced by an immutable object cannot be changed.
+= changes the list in place. Item assignment does not work, but when an exception occurs, the item has already been changed in place. a = 1 def some_func(): return a def another_func(): a += 1 return a Result:
>>> some_func() 1 >>> another_func() UnboundLocalError: local variable 'a' referenced before assignment Explanation
another_func local scope variable, but it was not previously initialized in the same scope that throws the error.global to modify the external scope variable a to another_func . def another_func() global a a += 1 return a Result:
>>> another_func() 2 e = 7 try: raise Exception() except Exception as e: pass Result (Python 2.x):
>>> print(e) # prints nothing Result (Python 3.x):
>>> print(e) NameError: name 'e' is not defined Explanation
Source of
If it assigns an exception to the target as , it is cleared at the end of the except clause. As if
except E as N: foo was converted to
except E as N: try: foo finally: del N This means that an exception must be assigned to another name, so that you can refer to it after the except clause. , (traceback), (reference cycle), , .
except . , . : def f(x): del(x) print(x) x = 5
y = [5, 4, 3]
**:** f (x)
UnboundLocalError: local variable 'x' referenced before assignment
f(y)
UnboundLocalError: local variable 'x' referenced before assignment
x
five
y
[5, 4, 3]- Python 2.x e `Exception()`, .
(Python 2.x):
>>> e Exception() >>> print e # Nothing is printed! def some_func(): try: return 'from_try' finally: return 'from_finally' Result:
>>> some_func() 'from_finally' try try…finally return , break continue , finally .return . finally , return , finally , . True = False if True == False: print("I've lost faith in truth!") Result:
I've lost faith in truth! bool ( 0 false 1 true). True , False bool , - True False — . >>> True is False == False False >>> False is False is False True >>> 1 > 0 < 1 True >>> (1 > 0) < 1 False >>> 1 > (0 < 1) False https://docs.python.org/2/reference/expressions.html#not-in
, a, b, c, ..., y, z — , op1, op2, ..., opN — , a op1 b op2 c… y opN z op1 b b op2 c … y opN z, , .
, a == b == c 0 <= x <= 100 .
False is False is False (False is False) and (False is False)True is False == False True is False and False == False , ( True is False ) False , False .1 > 0 < 1 1 > 0 and 0 < 1 , True .(1 > 0) < 1 True < 1 >>> int(True) 1 >>> True + 1 #not relevant for this example, but just for fun 2 1 < 1 False
one.
x = 5 class SomeClass: x = 17 y = (x for i in range(10)) Result:
>>> list(SomeClass.y)[0] 5 2
x = 5 class SomeClass: x = 17 y = [x for i in range(10)] (Python 2.x):
>>> SomeClass.y[0] 17 (Python 3.x):
>>> SomeClass.y[0] 5 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}) Result:
>>> print(some_list) None >>> print(some_dict) None , / (sequence/mapping objects) list.append , dict.update , list.sort . ., None . — , ( )
WTF, , Python . .
a = float('inf') b = float('nan') c = float('-iNf') #These strings are case-insensitive d = float('nan') Result:
>>> 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 'inf' 'nan' — ( ). float , , , «» « ».
one.
class A: x = 1 class B(A): pass class C(A): pass Result:
>>> Ax, Bx, Cx (1, 1, 1) >>> Bx = 2 >>> Ax, Bx, Cx (1, 2, 1) >>> Ax = 3 >>> Ax, Bx, Cx (3, 2, 3) >>> a = A() >>> ax, Ax (3, 3) >>> ax += 1 >>> ax, Ax (4, 3) 2
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] Result:
>>> 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 += . . 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!") (Python 2.x):
Caught! ValueError: list.remove(x): x not in list (Python 3.x):
File "<input>", line 3 except IndexError, ValueError: ^ SyntaxError: invalid syntax except . — , . Example: some_list = [1, 2, 3] try: # This should raise a ``ValueError`` some_list.remove(4) except (IndexError, ValueError), e: print("Caught again!") print(e) (Python 2.x):
Caught again! list.remove(x): x not in list (Python 3.x):
File "<input>", line 4 except (IndexError, ValueError), e: ^ IndentationError: unindent does not match any outer indentation level as . Example: some_list = [1, 2, 3] try: some_list.remove(4) except (IndexError, ValueError) as e: print("Caught again!") print(e) Result:
Caught again! list.remove(x): x not in list 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) Result:
('Time at noon is', datetime.time(12, 0)) The midnight time is not printed. Python 3.5 datetime.time False , UTC. - if obj : , obj null «».
one.
# A simple example to count the number of boolean 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 Result:
>>> booleans_found_so_far 0 >>> integers_found_so_far 4 2
another_dict = {} another_dict[True] = "JavaScript" another_dict[1] = "Ruby" another_dict[1.0] = "Python" Result:
>>> another_dict[True] "Python" int >>> isinstance(True, int) True >>> isinstance(False, int) True True 1 , False — 0 . >>> True == 1 == 1.0 and False == 0 == 0.0 True Python- .
t = ('one', 'two') for i in t: print(i) t = ('one') for i in t: print(i) t = () print(t) Result:
one two o n e tuple() t = ('one',) t = 'one' , ( ), , t str .tuple . some_string = "wtf" some_dict = {} for i, some_dict[i] in enumerate(some_string): pass Result:
>>> some_dict # An indexed dict is created. {0: 'w', 1: 'f', 2: 'f'} for Python : for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] exprlist — . , {exprlist} = {next_value} . @tukkek:
for i in range(4): print(i) i = 10 Result:
0 1 2 3 , ?
i = 10 - Python. , ( range(4) ), ( i ).enumerate(some_string) i ( A ) some_string . ( ) i some_dict . : >>> i, some_dict[i] = (0, 'w') >>> i, some_dict[i] = (1, 't') >>> i, some_dict[i] = (2, 'f') >>> some_dict x = True y = False Result:
>>> not x == y True >>> x == not y File "<input>", line 1 x == not y ^ SyntaxError: invalid syntax == , not .not x == y not (x == y) , not (True == False) , True .x == not y SyntaxError , (x == not) y , x == (not y) , .not — not in ( == not in ), not in , SyntaxError.PiaFraus .
a, b = a[b] = {}, 5 Result:
>>> a {5: ({...}, 5)} (target_list "=")+ (expression_list | yield_expression) and:
( , , ) , .
+ (target_list "=")+ , . a , b a[b] ( , , {} , 5 ).{} , 5 a , b , a = {} b = 5 .a {} , .a[b] ( , , a b . , a {} b 5 ).5 ({}, 5) , ( {...} , a ). : >>> some_list = some_list[0] = [0] >>> some_list [[...]] >>> some_list[0] [[...]] >>> some_list is some_list[0] [[...]] ( a[b][0] , a )
a, b = {}, 5 a[b] = a, b , a[b][0] ,
a >>> a[b][0] is a True Join() — (string operation), (list operation). .join() — , (, , ). , . , API list .[] = () ( tuple list )'a'[0][0][0][0][0] , Python .3 --0-- 5 == 8 --5 == 5 True . import dis exec(""" def f():* """ + """ """.join(["X"+str(x)+"=" + str(x) for x in range(65539)])) f() print(dis.dis(f)) >>> some_list = [1, 2, 3, 4, 5] >>> some_list[111:] [] ! :) CONTRIBUTING.md .
• https://www.youtube.com/watch?v=sH4XF6pKKmk
• https://www.reddit.com/r/Python/comments/3cu6ej/what_are_some_wtf_things_about_python
• https://sopython.com/wiki/Common_Gotchas_In_Python
• https://stackoverflow.com/questions/530530/python-2-x-gotchas-and-landmines
• https://stackoverflow.com/questions/1011431/common-pitfalls-in-python ( StackOverflow , Python.)
Source: https://habr.com/ru/post/337364/
All Articles