The world’s leading publication for data science, AI, and ML professionals.

Five Unexpected Behaviours of Python Could Be Surprised

Some cold knowledge about Python you need to know

Image by chriszwettler from Pixabay
Image by chriszwettler from Pixabay

Every Programming language may have some interesting facts or mysterious behaviours, so does Python. In fact, as a dynamic programming language, there are even more interesting behaviours in Python.

I would bet most of the developers may never experience one of these scenarios because most of us will write "regular" code. However, it is still good to know these things from others to remind ourselves to avoid some pitfalls. Also, we may learn from these examples to get a deeper understanding of Python.

In this article, I’ll demonstrate five of these interesting scenarios. If you want to explore more of them, here are another three 🙂

Three Mysterious Behaviours of Python

1. "+=" is Not an Atomic Operation

Image by Here and now, unfortunately, ends my journey on [Pixabay](https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=3237646) from Pixabay
Image by Here and now, unfortunately, ends my journey on [Pixabay](https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=3237646) from Pixabay

We know that a tuple in Python is an immutable object, so we can’t change the elements once it is created.

my_tuple = (1,2,3)
my_tuple[0] = 11

Well, that’s expected. However, a list in Python is mutable. So, we can add more elements to a list, even if it is an item inside a tuple. Not surprisingly, if we define a tuple with lists as items, we can extend one of the lists with more items.

my_tuple = ([1],[2],[3])
my_tuple[2].extend([4,5])

We have used the extend() method which will append a list to another. However, what if we try to use the "+=" operator to extend the list?

my_tuple[2] += [6,7]

Since tuples are immutable, it doesn’t allow us to replace the original list [3,4,5] with the new list [3,4,5,6,7]. However, if we try to print the tuple we will find that the list was changed!

This is because the operator "+=" is not an atomic action. The basic logic is as follows.

  1. Since we used "+=", Python will append the list [6,7] to the original list in the tuple. It will happen in place because we used "+=".
  2. It will try to assign the new list back to the tuple.

OK. The operation is not atomic and it consists of two steps: extending and assigning. The 2nd step assigning will fail because the tuple is immutable. However, the 1st step modified the list in place so that the list had been changed already.

That’s why we saw the error message but the list in the tuple had been changed.

2. Class Attribute is Stored in a Dictionary

Image by Luisella Planeta Leoni from Pixabay
Image by Luisella Planeta Leoni from Pixabay

We know that a class attribute can be defined inside a Python class. Because it is a class attribute (somehow similar to static property in other programming languages), we can access it without an instantiated object.

Let’s define a parent class and two child classes.

class Parent:
    class_attr = 'parent'
class Child1(Parent):
    pass
class Child2(Parent):
    pass

Since the two child classes were inherited from the parent class, they will inherit the class attribute, too. So, the attribute value of all these three will be the same.

print(Parent.class_attr, Child1.class_attr, Child2.class_attr)

Now, if we explicitly assign a value to the class attribute in a child class, it will show the difference.

Child1.class_attr = 'child1'

But if we changed the value from the parent class, all the other child classes that never overwrite this class attribute will reflect the change as well.

Parent.class_attr = 'parent_new'

This is because all these class attributes will be stored in a special dictionary for the class. We can access this dictionary using __dict__.

Parent.__dict__
Child1.__dict__
Child2.__dict__

If we don’t overwrite the attribute, there won’t be such a key-value pair in the dictionary of the child class. Then, it will try to find it in its parent class. If we do assign the class attribute with a value for the child class, it will maintain it in its dictionary.

3. Disappeared Error Message

Image by Steve Buissinne from Pixabay
Image by Steve Buissinne from Pixabay

Unlike most other programming languages, Python maximum expends the scope of variables. For example, if we define a variable in an if-else condition, it will be accessible from its outer scope.

if True:
    a = 1
print(a)

However, this is not true for a try-except block.

try:
    1/0
except ZeroDivisionError as e:
    a = 1
print('a =', a)
print('The exception was:', e)

In the above code, we intentionally triggered an exception and caught it in the except block. However, the message e is no longer accessible while the normal variable a still can be.

As stated in the official document:

When an exception has been assigned using as target, it is cleared at the end of 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. ref: https://docs.python.org/3/reference/compound_stmts.html#except

If we really want to keep that error message, we can still achieve it by assigning it to another variable as follows.

err_msg = None
try:
    1/0
except ZeroDivisionError as e:
    print('The exception is:', e)
    err_msg = e
print('The exception was:', err_msg)

4. Irreliable Value Passing

Image by Tommy_Rau from Pixabay
Image by Tommy_Rau from Pixabay

NumPy is one of the most widely used libraries in Python. It is good at modelling a multi-dimensional array. Now, let’s write a function to define a NumPy array but not return it.

import numpy as np
def make_array(num):
    np.array([num])

I know, it doesn’t make any sense to write such a function, but I just want to show the interesting behaviour.

Now, let’s use this function to create a NumPy array, then use the np.empty() function to create an empty array with only 1 value. Also, let’s explicitly specify that we want integers in the empty array.

make_array(12345)
np.empty((), dtype=np.int)

Why we can pick up the value that was created by the function?

In fact, the NumPy array had been created in the function. However, it wasn’t returned by the function so that the value of this array became "garbage". The memory address of this garbage value would be released. In other words, this memory address can be used for other purposes now.

Then, the np.empty() function will create an empty array with the value placeholders pointing to the memory. In this case, the most recent released memory will be used. That’s why we can see the value in the new empty array.

5. Invisible Iterating Conditions

Image by It is not permitted to sell my photos with StockAgencies from Pixabay
Image by It is not permitted to sell my photos with StockAgencies from Pixabay

Please be noticed that the code in this example is totally WRONG! it is for demonstration purposes only!

Let’s define a simple dictionary and then loop it. For each loop of this dictionary, we want to delete a key-value pair and create a new one. The code is as follows (again, the way of writing such code is WRONG!).

my_dict = {1: 1}
for key in my_dict.keys():
    del my_dict[key]
    my_dict[key+1] = key+1
    print(key)
    print(my_dict)

What’s the initial intuitive guessing? Will this be an infinite loop? No, the result is quite surprising.

We thought we wrote a bug and it should be an infinite looping. However, it stops at 6 every time (Python 3.7).

For a Python dictionary, it is usually created with a minimum length, which is 8. When it reaches 2/3 of the length, the dictionary will be automatically resized. It was the resizing that caused the failure of the iterating. 8 * (2/3) = 5.33, which is exactly the number we stopped.

When the resizing was triggered at the 6th item, that causes the "next" key is now slotted in an "earlier" slot.

Summary

Image by Trang Pham from Pixabay
Image by Trang Pham from Pixabay

In this article, I have demonstrated five interesting and mysterious behaviours in Python, including the class attributes, non-atomic "+=" operation, accessing the except error message in the outer scope, NumPy empty function and the dictionary. Hope these examples raised some of your interests, though some of them were totally wrong in terms of programming.

If you find the examples in this article interesting, I would find more examples and tricks like these in the future!

Join Medium with my referral link – Christopher Tao

If you feel my articles are helpful, please consider joining Medium Membership to support me and thousands of other writers! (Click the link above)


Related Articles