Class vs Instance Attributes in Python Explained

Python differentiates between class attributes and instance attributes through where they are defined, how they are stored in memory, and how its internal lookup mechanism resolves them. Class attributes belong to the class itself and are shared by all instances, while instance attributes are distinct to each individual object. Understanding this distinction relies on understanding Python's underlying namespace dictionaries (__dict__) and the order in which Python searches for attributes.

Definition and Namespace Storage

The primary syntactic difference lies in where the attribute is defined:

class Car:
    wheels = 4  # Class attribute

    def __init__(self, color):
        self.color = color  # Instance attribute

In this example, Car.__dict__ contains 'wheels': 4, while an instance my_car = Car("red") has its own dictionary my_car.__dict__ containing 'color': 'red'.

The Lookup Chain

When you access an attribute via an instance (for example, my_car.attribute), Python uses a specific resolution order:

  1. Instance Namespace: Python first inspects the instance's __dict__. If the attribute is present, Python returns its value immediately.
  2. Class Namespace: If the attribute is not found in the instance, Python inspects the class's __dict__. If present, it returns the class attribute.
  3. Parent Classes: If not found in the class, Python traverses the method resolution order (MRO) through any base classes.
  4. Attribute Error: If the attribute is not found anywhere in the hierarchy, Python raises an AttributeError.

Because of this order, an instance can read a class attribute directly (my_car.wheels evaluates to 4) as long as the instance does not define an attribute with the same name.

Assignment and Shadowing

Python distinguishes clearly between read operations and write operations on instance references:

If you execute my_car.wheels = 6, Python does not update the class attribute. Instead, it creates a new instance attribute named wheels inside my_car.__dict__. From that point forward, my_car.wheels returns 6 because the instance namespace takes precedence over the class namespace. This behavior is known as shadowing.

Other instances of Car remain unaffected and still reference the class attribute wheels = 4. To explicitly modify a class attribute for all instances, you must assign to it using the class name directly: Car.wheels = 6.