DEV Community

Cover image for 5 WTF Moments in Python
Zane Chen
Zane Chen

Posted on • Edited on • Originally published at zane-portfolio.kiyo-n-zane.com

5 WTF Moments in Python

1. It Is Not the Function I Called!

I encountered this issue when implementing the Decorator Pattern in python.

callbacks = [
    callback1,
    callback2,
    callback3,
]
wrapped = []

for callback in callbacks:
    def wrapped_callback(*args, **kwargs):
        print(f"{callback.__name__} is invoked.")
        callback(*args, **kwargs)

    wrapped.append(wrapped_callback)

for wrapped_cb in wrapped:
        wrapped_cb()
Enter fullscreen mode Exit fullscreen mode
callback3 is invoked.
callback3 is invoked.
callback3 is invoked.
Enter fullscreen mode Exit fullscreen mode

Surprised? This would likely break heads of developers famaliar with other languages. The rationale, however, is not complex. When defining function in Python, variables inside the closure are captured by reference, not by value. This means the values of callback in each wrapped_callback are resolved during execution time, not definition time. When the interpreter looks up the value of callback for each wrapped_callback, callback always holds the value of callback3, since it ended up with the value when exiting the loop.

To avoid this, simply avoid defining function inside loops.

callbacks = [
    callback1,
    callback2,
    callback3,
]

wrapped = []
def wrap_callback(callback):
    def wrapped(*args, **kwargs):
        print(f"{callback.__name__} is invoked.")
        callback(*args, **kwargs)

    return wrapped

for callback in callbacks:
    wrapped.append(wrap_callback(callback ))

for wrapped_cb in wrapped:
    wrapped_cb()
Enter fullscreen mode Exit fullscreen mode
callback1 is invoked.
callback2 is invoked.
callback3 is invoked.
Enter fullscreen mode Exit fullscreen mode

As a side note, be careful when using lambda functions in Python as well. They're convenient but can cause the same problem if used inappropriately.

funcs = [lambda: print(f"No.{i}") for i in range(3)]
for func in funcs:
    func()
Enter fullscreen mode Exit fullscreen mode
No.2
No.2
No.2
Enter fullscreen mode Exit fullscreen mode

2. Mutable Default Function Parameters Can Bite you

If you ever used default parameter in compiled languages like C#, the compiler forced the default value to be a compile time value. Although Python is an interpreted language, it still binds the default value at the function definition time, which means the default value got created only once. If the value is mutable and gets modified, subsequent calls will use the modified value.

def append_1_to_list(l = []):
    l.append(1)
    print(l)

append_1_to_list()  # [1]
append_1_to_list()  # [1, 1]
Enter fullscreen mode Exit fullscreen mode

Solution? Avoid this pattern. If you really need this, make it default to None and intialize the value in function body.

3. “Finally” Is Literally Your Final Behavior

def will_throw():
    try:
        raise Exception()
    finally:
        return "Will you see me?"

will_throw() # 'Will you see me?'
Enter fullscreen mode Exit fullscreen mode

Python silently eats the exception! This is more than confusing. I haven't found a satisfying explanation for why this happens. But the behavior here is that, the interpreter stores the error temporarily before the finally statement executed. If a return statement appears in finally, the exception will be discarded.

Similarly, finally is remarkably greedy, it will even discarded your previous return value if any.

def who_should_i_see():
    try:
        return "You should see me."
    finally:
        return "Will you see me?"

who_should_i_see()  # 'Will you see me?'
Enter fullscreen mode Exit fullscreen mode

4. The Method Is not the Method

class MyClass:
    def the_method(self): pass

obj = MyClass()
obj.the_method is obj.the_method  # False
Enter fullscreen mode Exit fullscreen mode

To understand this behavior, let's examine what happens under the hood when we use obj.the_method.

Instead of solving the puzzle directly, try answer another question, what is self meant for in the method parameter? We are taught that self representsthe object the method is bound to, which is why self doesn't appear in the method signature when we call methods on objects. Who fill the self parameter with the object? Magic, and it happened when the method is accessed or called.

This binding happens every time we access obj.the_method, creating a new method object from the function definition and the bound object every time. This explains why obj.the_method is obj.the_method evaluates to False.

While we won't dive deeper in this post, it's worth learning about this comprehensively. Python object attributes use something called Discriptor to control access behavior, which is where the magic happens. Then Python official doc is the best place to learn more.

5. True Is an Integer

isinstance(True, int)  # True
isinstance(True, bool)  # True
Enter fullscreen mode Exit fullscreen mode

Surprise again! Booleans are integers! Both True and False and instances of both bool and int. You might wonder about the relationship between bool and int.

issubclass(bool, int)  # True
issubclass(int, bool)  # False
Enter fullscreen mode Exit fullscreen mode

Yeah, bool is a subclass of int. It's hard to believe that a "modern language" would have this quirk, but remember that. But remember, Python is 32-year-old man, at least older than me. Python was created without a boolean type, just like old C, and developers historically use 0 and 1 instead. For the backward compatibility, bool became a subclass of int when it was introduced.

Top comments (0)