0
class Animal:
    x = {}
    y = 0
    def __init__(self):
        pass


animal1 = Animal()
animal2 = Animal()

animal1.x['num'] = 14
animal1.y = 14

print(animal2.x)
print(animal2.y)

Output:

{'num': 14}

0

Why is a dictionary treated as a class variable but an int is unique for each instance? I'm still pretty new to Objects and Classes I was just messing around with things and noticed this.

I expected both to remain unchanged

2
  • animal1.x['num'] = 14 is updating. But animal1.y = 14 is overriding since an integer is an immutable type. But still I expected to change take effect for both objects.... Commented May 9, 2024 at 23:25
  • @MSH: It has nothing to do with int being an immutable type. int being immutable just means there is no way to modify it in-place, so rebinding is the only option. animal1.x = {'num': 14} would rebind in just the same way that animal1.y = 14 is doing, even though dicts are mutable, what matters is whether you're rebinding the attribute (which causes auto-vivification of an instance attribute) or reading the object out of the attribute and then modifying it in-place. Commented May 9, 2024 at 23:37

3 Answers 3

1

Defining the variables like you did makes them class variables. If you want to guarantee a fresh copy for every instance, you'd do something like

class Animal:
    def __init__(self):
        self.x = {}
        self.y = 0

In your case the definition

class Animal:
    x = {}
    y = 0

means that every instance of the class Animal gets the identical reference to an empty dictionary, there are not multiple dictionaries. If you add to that dictionary, every instance will know. If you wanted to give an instance it's own version, you could technically do animal1.x = {'num': 14}. This way you create a new dictionary and assign it. This won't affect animal2.

Sign up to request clarification or add additional context in comments.

5 Comments

This is all generally good practice, but the OP already seems to understand the distinction between class and instance variables, and clearly was asking about the name-shadowing behavior, which you only address in brief near the end.
You also have a slightly misleading wording here when you say the class attribute version "means that every instance of the class Animal gets the identical reference to an empty dictionary". Each instance is not getting an identical reference to the empty dictionary. There is only one reference, on the class, and when you look up the attribute on the instance, and the instance doesn't have it, it reaches back to the class to find that single unique reference.
I'm not so sure that the OP understands the distinction. They wrote "I'm still pretty new to Objects and Classes". My impression was that they noticed one variable acting in a shared way and one variable acting in a non-shared way and then used the terms "class" and "instance" because they seemed to fit. In particular the "I expected both to remain unchanged" seems to hint at expecting instance variables by default, despite the class definition. If I'm completely off the mark for OPs knowledge level, I'm happy if they prefer your answer.
The title of the question being "Python Class Variables Vs Instance Variables" implies needed clarification on the subject.
Hello thanks for the explanations I understand defining them outside any constructors should lead to them being shared class variables but it seems like when defining the int value it is unique even if not defined in a constructor, but the dict value is not. I was confused on that behavior.
1

Class variables can be difficult to understand at first. They are essentially variables that are shared between all instances of a class. Two practical applications of this is are as an instance counter, or as default settings for all objects. If they are changed through the definition itself (Animal.y = 14) they will be updated for all instances and future instances.

This contrasts entirely with instance variables which are always specific to and only accessible through their respective object.

However, the problem with the example you provided is that you are confusing assignment with mutation. You mutated the dictionary whereas you assigned the integer. To assign the dictionary the same way you assigned 14 to the y variable you would have to create a new dictionary like so:

animal1.x = {'num': 14}

This way it will not affect the animal2 object.

2 Comments

Wow… thank you, I’ll definitely read more about that. So I assume if I were to use any mutable datatype in that same way id yield the same shared result.(Im asking b/c im not at computer yet to test it out)
Yeah, that's right. Mutating any mutable datatype in the same way you did in your example will yield the same shared result as the variable was never actually reassigned just mutated. This is because, in your example, the dictionary actually remained the same dictionary you simply added a key/value pair to it (thus mutating it).
1

It's not about the types, it's about rebinding and name shadowing. When you assign directly to an attribute of an instance, you bind an attribute of that name on the instance. So animal1.y = 14 is saying "for this specific instance, it should have an attribute named y with the value 14". It doesn't matter that the class had an attribute of the same name, those are only found as a fallback when reading an attribute (there's some weirdness with the descriptor protocol I'm glossing over here, but that's the general rule).

By contrast, when you say animal1.x['num'] = 14 you are not writing to x. You are reading from animal1.x (which, since the instance lacks that attribute, it's read from the class), then writing into the thing you just read. The behavior is exactly the same as if you said:

xalias = animal1.x
xalias['num'] = 14

animal1.x is clearly not being written in that broken up version, and it's not being written to in the compact version either, you just modified the dict through an alias. You've told Python "please load from animal1.x, then assign 14 to the key 'num' within whatever you just loaded."

If you had instead said animal1.x = {'num': 14} ("for this specific instance, it should have an attribute named x with the value {'num': 14}"), it would behave just like it did for animal1.y = 14; the instance would get its own shadowing attribute named x that hides the x from the class, with it's own separate dictionary, because you bound a new object ("wrote") to the attribute on the instance.

This behavior shares certain similarities with the scoping rules for functions. If you have a function that never writes to a variable of a given name, only reads from it, it will read that variable from an outer (enclosing, global, or builtin) scope. If you assign to that variable though (without using global or nonlocal declarations to override the default behavior), it becomes a local, and its not possible to read the variable of that name from outer scopes without jumping through hoops. In the case of function scope, this choice is statically determined when the function is compiled (the variable is either local or nonlocal for the entire body of the function), the only difference with instance attributes shadowing class attributes is that it occurs dynamically (the instance attribute can be created lazily and only begins shadowing the class attribute when it's actually created).

It's also easier to work around the issue for instance/class attributes than for local/nonlocal variables; if you wanted the assignment animal1.y = 14 to replace the class attribute instead, even if you didn't know what class animal1 came from, you could do type(animal1).y = 14. That will show the change in animal2.y just fine, because you modified the original class (type(animal1) is returning Animal itself here), not an instance of it.

2 Comments

Golden Response, Ill look into shadowing more and play around with it as well and see if I can know the output before running the code. explicitly defining them in constructors should be the appropriate way of doing it. I just wanted to test out different cases! Appreciate you.
@MichaelOjoko: Yeah, the shadowing behavior can be extremely non-intuitive, and it's especially tricky with augmented assignment operators (e.g. +=), because: 1) The behavior for augmented assignment differs for mutable vs. immutable types (you just have to memorize which types are mutable or not), but 2) Shadowing actually occurs with += no matter what, but for mutable types, you shadow the class variable with an instance variable that is an alias of the same object, which is confusing (you only notice it happening if you later replaced the class attribute; the instance stays unchanged).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.