Weird Python Array Pitfall (or, I Didn't Know How Python Classes Work)

So it seems that if you create a class and declare an array in that class outside the __init__() function, the array will be treated like a class variable (even though you must refer to it as self.array). But it will be treated as an instance variable if you declare it inside the __init__().

To be clear, this doesn’t appear to be a Rhino issue but rather a “feature” of the python language (unless I’m doing something wrong). So, this post is more of a caution or helpful info. It had me very puzzled for several hours. It was happening with a custom event, which made me think it had something to do with passing functions between classes or something, but, no, my subscriber list (of handlers) was an array declared outside of the __init__ of my custom class. So all instances would fire the event even though I only subscribed to the event of one instance.

Here is a simple example demonstrating the issue. If you run it, the results (posted below the example) show that that List1 ends up appending words from both instances, while List2 has a separate list for each instance.

#! python3

class WordListCollection:    
    # this will be treated as a class variable
    List1 = []
    Name = ""
    def __init__(self):
        # this will be treated as an instance variable
        self.List2 = []      

    def AddWordsToList1(self, words):
        for word in words:
            self.List1.append(word)

    def AddWordsToList2(self, words):
        for word in words:
            self.List2.append(word)


WLC1 = WordListCollection()
WLC1.Name = "WordListCollection1"
WLC1.AddWordsToList1(["apple", "pear", "banana"])
WLC1.AddWordsToList2(["Ford", "Chevy", "Dodge"])

WLC2 = WordListCollection()
WLC2.Name = "WordListCollection2"
WLC2.AddWordsToList1(["peach", "cherry", "apricot"])
WLC2.AddWordsToList2(["Mercury", "Pontiac", "Jeep"])


print(WLC1.Name)
print("List1", WLC1.List1)
print("List2",WLC1.List2)

print("")
print("--------------")
print("")

print(WLC2.Name)
print("List1", WLC2.List1)
print("List2", WLC2.List2)

Results:

WordListCollection1
List1 ['apple', 'pear', 'banana', 'peach', 'cherry', 'apricot']
List2 ['Ford', 'Chevy', 'Dodge']

--------------

WordListCollection2
List1 ['apple', 'pear', 'banana', 'peach', 'cherry', 'apricot']
List2 ['Mercury', 'Pontiac', 'Jeep']

That is right: Python types can have both class and instance attributes which can both be accessed internally through self. When you use self.___ to lookup an attribute, python will first look for instance attributes with that name on the object, and then if it does not find one on the instance will go look on the class (an so on up the class’s inheritance tree, though it gets weirder if you have multiple inheritance…).

You can see more about python’s lookup behavior here if you like: link

If you are interested, I can also recommend checking out folks like Corey Shafer who have some great video series on the basic use and behavior of the Python language.

There is specifically a good one on class-variables which is relevant to your case here:

best,
@ed.p.may

1 Like

As already mentioned, it is not a “feature”, it is a feature.

1 Like

Thanks! That video was perfect. I understand much better now. I was puzzled because I declared Name outside the __init__() as well and it seemed to act like an instance variable. I now see that it did in fact become an instance variable the moment I set it in the instance. But with the array, I set it in the class and only ever append to it; so, it stays a class variable.

This means if I set the array on the instance (WLC1.List1 = []), it becomes an instance variable. And because I used self.List1 in the AddWordsToList1 function, I can use that function to add words to the instance variable, but if I refer to List1 in the function as WordListCollection.List1, the function will only add words to the class array and not to the instance array (I tested all this and that is exactly how it works).

I have written a LOT of code under the assumption that variables listed outside the __init__ were instance variables. And, by chance, it all worked fine, until this morning. Nice to have this clarified.

(I didn’t think I had a problem that needed a solution…but apparently I did, so I marked your post as the solution)

1 Like

Don’t forget to read The Python Tutorial — Python 3.13.0 documentation before bedtime.

And put The Python Language Reference — Python 3.13.0 documentation under your pillow, along with The Python Standard Library — Python 3.13.0 documentation .

Ah well, for Rhino 8 you’d best of switching the docs to 3.10 so you don’t end up using stuff that isn’t in that version yet :slight_smile:

2 Likes