The earlier classes and objects article introduced instance vs class variables python at a basic level — this one goes deeper into how Python actually resolves attribute lookups, the mutable class variable bug that trips up even experienced developers, and how to deliberately inspect exactly where a given value lives.
Introduction: Two Places a Variable Can Live
A variable attached to a class can live in one of two places, and the distinction matters more than it might first appear.
Class variables are defined directly in the class body, outside any method — and they're shared by every instance of that class.
Instance variables are defined on self (typically inside __init__) — and they're unique to each individual object.
class Animal:
species = "Unknown" # class variable
def __init__(self, name):
self.name = name # instance variableHow lookup actually works
When you access an attribute through an instance — animal.species — Python checks the instance's own namespace first. If it doesn't find the attribute there, it falls back to checking the class's namespace next. This lookup order is the key mechanic behind nearly everything covered in this article, including the shadowing behavior discussed in Section 4.
Class Variables in Practice
A basic example
class Animal:
species = "Unknown"
def __init__(self, name):
self.name = name
dog = Animal("Rex")
cat = Animal("Whiskers")
print(dog.species) # Unknown — accessed through an instance
print(cat.species) # Unknown — same shared value
print(Animal.species) # Unknown — also accessible directly through the class itselfNotice species is accessible three different ways here — through either instance, or directly through the class — and all three currently point to the exact same shared value.
Good use cases for class variables
Constants that genuinely apply to every instance equally, and shouldn't vary between objects.
Shared configuration values, like a default timeout or a base URL, that every instance of a class should reference consistently.
Default labels or categories — the
speciesexample above is a reasonable fit for this.A counter tracking how many instances have been created:
class Animal:
count = 0
def __init__(self, name):
self.name = name
Animal.count += 1 # incrementing the shared class variable, not creating an instance one
Animal("Rex")
Animal("Whiskers")
Animal("Buddy")
print(Animal.count) # 3This works specifically because the increment targets Animal.count explicitly — writing self.count += 1 instead would actually trigger the shadowing pitfall covered in Section 4, so this pattern is worth internalizing exactly as written.
Comparison to statically typed languages
If you've used Java or C++, Python class variables will feel conceptually similar to static fields in those languages — a single value shared across every instance. But Python's version is considerably more flexible, and less strictly enforced, in two specific ways: any individual instance can freely shadow a class variable with its own instance-level version (covered in Section 4), and there's no fixed, enforced type — a class variable can be reassigned to hold a value of a completely different type at any point, since Python remains dynamically typed throughout.
Instance Variables in Practice
A basic example
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
alex = Person("Alex", 30)
sam = Person("Sam", 25)
print(alex.name, alex.age) # Alex 30
print(sam.name, sam.age) # Sam 25name and age are set independently for each object, through self inside __init__ — alex and sam genuinely hold separate values, with no connection between them.
Why instance variables fit per-object state
Instance variables are the right tool any time a value genuinely needs to vary independently across every object of a class — a bank account's balance, a player's score, a sensor's most recent reading. These are all things where it would be a real bug if one object's value affected another's:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alex", 100)
account2 = BankAccount("Sam", 50)
account1.balance += 20
print(account1.balance) # 120
print(account2.balance) # 50 — correctly unaffectedThe Classic Pitfall: Mutable Class Variables
The bug
This is the single most common, most genuinely surprising mistake involving class variables: if a class variable is a mutable object — a list or dictionary — every instance shares that exact same object, not independent copies. Modifying it through one instance's reference affects every other instance too.
class ShoppingCart:
items = [] # a mutable class variable — DANGER
def add_item(self, item):
self.items.append(item)
cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.add_item("apple")
print(cart2.items) # ['apple'] — cart2 sees cart1's item, unexpectedly!self.items.append(...) doesn't create a new instance variable — it looks up items (found via the class, since no instance-level items exists yet), and calls .append() directly on that single, shared list object. Both cart1 and cart2 end up pointing at the same underlying list.
The fix: move it into init
class ShoppingCart:
def __init__(self):
self.items = [] # now a genuine instance variable — a fresh list per object
def add_item(self, item):
self.items.append(item)
cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.add_item("apple")
print(cart1.items) # ['apple']
print(cart2.items) # [] — correctly independentMoving the list's creation into __init__, assigned via self.items = [], ensures every new object gets a genuinely separate list, rather than all objects pointing back to one shared list defined once in the class body.
Related shadowing behavior
This is worth understanding precisely, since it explains why the fix above works, and clarifies a related but distinct scenario: assigning to self.attr = value never modifies a class variable — it always creates a brand-new instance attribute that shadows the class variable of the same name, for that one specific object only.
class Animal:
species = "Unknown"
rex = Animal()
rex.species = "Dog" # this creates a NEW instance attribute on rex, doesn't touch Animal.species
print(rex.species) # Dog — rex's own instance attribute
print(Animal.species) # Unknown — the class variable is completely untouchedCompare that to modifying the class variable directly, through the class name itself — which genuinely changes it for every instance that hasn't already shadowed it with their own instance-level version:
Animal.species = "Mammal" # genuinely modifies the shared class variable
other = Animal()
print(other.species) # Mammal — picks up the updated class variable
print(rex.species) # Dog — rex still has its own shadowing instance attribute, unaffectedReading, Modifying, and Inspecting Variables
Changing for everyone vs. changing for one
To summarize the two distinct operations shown above: ClassName.attr = value modifies the shared class variable, affecting every instance that hasn't shadowed it. instance.attr = value creates (or updates) an instance-level attribute specific to that one object, shadowing the class variable for that instance alone, without touching it for anyone else.
How this plays out under inheritance
Subclasses inherit their parent class's class variables automatically, and can override them simply by redefining the same name at their own level:
class Animal:
category = "Animal"
class Dog(Animal):
pass
class Bird(Animal):
category = "Flying Animal" # overrides the inherited class variable
print(Dog().category) # Animal — inherited from Animal, unchanged
print(Bird().category) # Flying Animal — Bird's own overrideDog doesn't define its own category, so Python's attribute lookup falls back through the inheritance chain and finds Animal's version. Bird defines its own category directly, which takes priority over the inherited one — the same shadowing principle from Section 4, just occurring at the class level (between a subclass and its parent) rather than between an instance and its class.
A practical debugging tip: inspecting dict
Every object and every class maintains its own __dict__, holding exactly the attributes actually defined at that specific level — a genuinely useful way to see precisely where a given value actually lives, rather than guessing based on behavior alone:
class Animal:
species = "Unknown"
def __init__(self, name):
self.name = name
rex = Animal("Rex")
print(rex.__dict__) # {'name': 'Rex'} — only instance attributes appear here
print(Animal.__dict__.keys()) # includes 'species', '__init__', and other class-level membersNotice species doesn't appear in rex.__dict__ at all — because it's never been assigned directly on rex; it's purely inherited from the class at lookup time. If you ever assigned rex.species = "Dog", then rex.__dict__ would show {'name': 'Rex', 'species': 'Dog'}, confirming the shadowing instance attribute now genuinely exists on that specific object. This kind of direct inspection is a genuinely reliable way to settle any confusion about whether a particular value is coming from an instance or falling back to its class — rather than relying on inference from behavior, you can simply look.