Instance vs Class Variables in Python

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 variable
How 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 itself

Notice 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 species example 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)   # 3

This 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 25

name 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 unaffected

The 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 independent

Moving 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 untouched

Compare 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, unaffected

Reading, 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 override

Dog 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 members

Notice 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.