Python Quirks
Here’s a catalogue of some fun “unintuitive” behaviour in Python.
Integer Memory Preallocation
256 is int("256")257 is int("257")This is because Python preallocates the integers from -5 to 256 at initialisation, meaning they are always at the same address in memory, while integers beyond this range can have multiple different references.
Floating Point Weirdness
int((3.0 * 0.1 - 0.3) ** (-0.1))This maps to an integer, despite the fact that is undefined. Can you guess the integer?
In fact, it is forty-two (42). This is because 0.3 is not representable exactly in binary.
Lambda Arguments Are Captured at Definition Time
funcs = [(lambda: i) for i in range(3)][f() for f in funcs]# [2, 2, 2] rather than [0, 1, 2]The i in the lambda definition references the variable, rather than its value at that point in time!
Default Arguments Can Be Mutated
def append_to_list(item, my_list=[]): my_list.append(item) return my_list# now run the function twiceappend_to_list(1) # [1] as expectedappend_to_list(2) # [1, 2] rather than [2]The list my_list=[] in the function definition defines an empty list. Since lists are mutable, the first time we call the function, 1 is appended to this default list, and remains there indefinitely.
Attributes Have Their Name Mangled
class MyClass: def __init__(self): self.__private = 42# create a member of the classobj = MyClass()obj.__private# AttributeError: 'MyClass' object has no attribute '__private'obj._MyClass__private # 42When beginning attribute names with a double underscore, their names are mangled to mark them as private.
And / Or
'a' and 'b' # 'b''a' or 'b' # 'a'In Python, left and right is actually equivalent to right if left else left: it returns the first false-like argument or the last true argument. Conversely, left or right is equivalent to left if left else right.
String Interning
a = "hello"b = "hello"a is b # Truea = "hello world"b = "hello world"a is b # FalseAs an optimisation technique, Python stores single words and other short strings in a string intern pool to re-use their addresses in memory.
Bonus Quirk: JavaScript
['0'].map(parseInt)['1', '0'].map(parseInt)['2', '1', '0'].map(parseInt)['3', '2', '1', '0'].map(parseInt)['4', '3', '2', '1', '0'].map(parseInt)['5', '4', '3', '2', '1', '0'].map(parseInt)['6', '5', '4', '3', '2', '1', '0'].map(parseInt)['7', '6', '5', '4', '3', '2', '1', '0'].map(parseInt)Try these in turn, trying to predict each before you do it. You can look at the previous results before guessing. They map to:
[0] for just 0[1, NaN] for 1 down to 0[2, NaN, 0] for 2 down to 0[3, NaN, 1, 0] for 3 down to 0[4, NaN, NaN, 1, 0] for 4 down to 0[5, NaN, NaN, 2, 1, 0] for 5 down to 0[6, NaN, NaN, NaN, 2, 1, 0] for 6 down to 0[7, NaN, NaN, NaN, 3, 2, 1, 0] for 7 down to 0
This is because parseInt takes a second argument for the radix (base): by default the list index.