Weird behaviour with python in jupyter notebook; is this a bug or should I just accept it as it is?

I was tinkering on a little project idea in jupyter notbook when I stumbled across some weird behaviour… The following code is abstracted from the original.

class MyClass:
    Instances = []
    
    def __init__(self,name=None):
        self.id = len(MyClass.Instances)
        MyClass.Instances.append(self)
        
        if name is None:
            self.name = 'Class %s' % self.id
        else:
            self.name = name
    
    def show(self):
        print('Name: %snId: %s' % (self.name, self.id))

    def instance_at(i : int):
        if i >= len(MyClass.Instances):
            raise ValueError("Instance does not exist")
        return MyClass.Instances[i]

(I hope the code is self-explanatory)

I ran the cell and tested the code and it work fine:

In [24] :   m = MyClass()
            m.show()

Out [25] :  Name: Class 0
            Id: 0

The Twist:

I didn’t like that the first instance, with no name given, would be called ‘Class 0’ so I thought to myself: ‘Why not add an object which will act as a placeholder for the index 0′ (Don’t ask why I did this, ’twas a brainfart).

So I changed line 2 to be Instances = [MyClass(name='id'). This also worked but when I tried to receive the instance at index 0, it’s id value was different to what I expected.

In [24] :   m = MyClass()
            m.show()
            MyClass.instance_at(0).show()

Out [25] :  Name: Class 0
            Id: 1
            Name: id
            Id: 1

This was the point where I decided to write a more generalized version of my code (the one shown here) in another notebook. I wrote it whole before running the cell, including Instances = [MyClass(name='id')].

This time I got this:

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-60020e1975a4> in <module>
----> 1 class MyClass:
      2     Instances = [MyClass(name='id')]
      3     #Instances = []
      4 
      5     def instance_at(i : int):

<ipython-input-1-60020e1975a4> in MyClass()
      1 class MyClass:
----> 2     Instances = [MyClass(name='id')]
      3     #Instances = []
      4 
      5     def instance_at(i : int):

NameError: name 'MyClass' is not defined

So now I have a piece of code which runs in one notebook but not in another. At least when you simply copy and paste it. In jupyter notebook this is fixable by changing line 2 to Instances = [], running the cell and changing it back.

I am fairly certain that this is because the class and the class variable Instances already existed before I created the ambiguous line of code.

In hindsight, this does make sense and I suspect the unexpected id value comes from a constructor call being discovered in the list when the instance m is being created.

Am I wrong? Can anybody elaborate?

Please let me know if a post like this is inappropriate here.

Answer

A simpler example:

class Example:
    def __init__(self):
        print("Creating an instance of the OLD class")

class Example: # redefining like this does **not** cause an error
    Instances = [Example()]
    def __init__(self):
        print("Creating an instance of the NEW class")
    
# the OLD message is printed immediately
# because `Instances = [Example()]` uses the previous definition
# because it **cannot** use the current one; it hasn't been created yet.

# You **do** get an error **without** the old definition, because then
# there isn't a definition at all.

x = Example() # the NEW message is printed
# the OLD class **still exists**, but cannot easily be accessed.
# As long as we can think of a way to get at an instance,
# we can use the `__class__` of the instance to create more;
# and we can rename that to make it easily usable:
Old_Example = Example.Instances[0].__class__
y = Old_Example()