Python - Instance vs Class Variables

• Instance variables are unique to each object and stored in `__dict__`, while class variables are shared across all instances and stored in the class namespace

Key Insights

• Instance variables are unique to each object and stored in __dict__, while class variables are shared across all instances and stored in the class namespace • Python’s attribute lookup follows the MRO (Method Resolution Order): instance → class → parent classes, which can lead to unexpected behavior when mixing variable types • Mutable class variables create a common pitfall where modifications affect all instances, making them unsuitable for default collections or objects

Understanding the Fundamental Difference

Instance variables belong to individual objects, while class variables belong to the class itself. This distinction affects memory allocation, scope, and how modifications propagate across your application.

class User:
    user_count = 0  # Class variable
    
    def __init__(self, name):
        self.name = name  # Instance variable
        User.user_count += 1

user1 = User("Alice")
user2 = User("Bob")

print(user1.name)  # Alice (instance-specific)
print(user2.name)  # Bob (instance-specific)
print(User.user_count)  # 2 (shared across all instances)
print(user1.user_count)  # 2 (accessed through instance)

Each User object maintains its own name in the instance’s __dict__. The user_count exists once in the class namespace, shared by all instances.

Attribute Lookup Chain and Shadowing

Python searches for attributes in a specific order. When you access instance.attribute, Python checks the instance dictionary first, then the class, then parent classes following the MRO.

class Config:
    timeout = 30  # Class variable
    
    def __init__(self, name):
        self.name = name

config1 = Config("primary")
config2 = Config("secondary")

print(config1.timeout)  # 30 (found in class)
print(config2.timeout)  # 30 (found in class)

# Shadow the class variable with an instance variable
config1.timeout = 60

print(config1.timeout)  # 60 (found in instance)
print(config2.timeout)  # 30 (still found in class)
print(Config.timeout)   # 30 (class variable unchanged)

# Verify the shadowing
print(config1.__dict__)  # {'name': 'primary', 'timeout': 60}
print(config2.__dict__)  # {'name': 'secondary'}

This shadowing mechanism is powerful but can introduce bugs. Modifying what appears to be a class variable through an instance actually creates a new instance variable, leaving the class variable intact.

The Mutable Class Variable Trap

Mutable class variables are a frequent source of bugs. When you use lists, dictionaries, or custom objects as class variables, all instances share the same object reference.

class ShoppingCart:
    items = []  # DANGEROUS: Mutable class variable
    
    def __init__(self, customer):
        self.customer = customer
    
    def add_item(self, item):
        self.items.append(item)

cart1 = ShoppingCart("Alice")
cart2 = ShoppingCart("Bob")

cart1.add_item("Book")
cart2.add_item("Laptop")

print(cart1.items)  # ['Book', 'Laptop'] - UNEXPECTED!
print(cart2.items)  # ['Book', 'Laptop'] - UNEXPECTED!
print(cart1.items is cart2.items)  # True - Same object

The correct approach initializes mutable objects as instance variables:

class ShoppingCart:
    def __init__(self, customer):
        self.customer = customer
        self.items = []  # Instance variable
    
    def add_item(self, item):
        self.items.append(item)

cart1 = ShoppingCart("Alice")
cart2 = ShoppingCart("Bob")

cart1.add_item("Book")
cart2.add_item("Laptop")

print(cart1.items)  # ['Book']
print(cart2.items)  # ['Laptop']

Legitimate Use Cases for Class Variables

Class variables excel at storing class-level constants, configuration, and shared state that should genuinely affect all instances.

class Database:
    connection_pool = []  # Shared resource
    max_connections = 10  # Configuration
    query_count = 0       # Shared counter
    
    def __init__(self, name):
        self.name = name
    
    @classmethod
    def execute_query(cls, query):
        cls.query_count += 1
        # Execute query logic
        return f"Executed: {query}"
    
    @classmethod
    def get_stats(cls):
        return {
            'total_queries': cls.query_count,
            'max_connections': cls.max_connections
        }

db1 = Database("primary")
db2 = Database("replica")

Database.execute_query("SELECT * FROM users")
Database.execute_query("INSERT INTO logs")

print(Database.get_stats())  # {'total_queries': 2, 'max_connections': 10}

Modifying Class Variables Safely

When you need to modify class variables, use the class name or cls parameter to avoid shadowing. Direct modification through instances creates instance variables instead.

class APIClient:
    request_count = 0
    rate_limit = 100
    
    def __init__(self, api_key):
        self.api_key = api_key
    
    def make_request(self):
        # Correct: Modify through class name
        APIClient.request_count += 1
        
        if APIClient.request_count > APIClient.rate_limit:
            raise Exception("Rate limit exceeded")
    
    @classmethod
    def reset_counter(cls):
        # Correct: Modify through cls parameter
        cls.request_count = 0
    
    def wrong_increment(self):
        # WRONG: Creates instance variable
        self.request_count += 1

client1 = APIClient("key1")
client2 = APIClient("key2")

client1.make_request()
client2.make_request()

print(APIClient.request_count)  # 2 (correct)

client1.wrong_increment()
print(client1.request_count)  # 1 (instance variable)
print(APIClient.request_count)  # 2 (class variable unchanged)

Memory and Performance Implications

Class variables consume memory once per class, while instance variables consume memory for each object. For large-scale applications with thousands of instances, this difference matters.

import sys

class WithClassVar:
    constant_data = "x" * 1000  # Shared
    
    def __init__(self, id):
        self.id = id

class WithInstanceVar:
    def __init__(self, id):
        self.id = id
        self.constant_data = "x" * 1000  # Duplicated

# Create 1000 instances
class_var_instances = [WithClassVar(i) for i in range(1000)]
instance_var_instances = [WithInstanceVar(i) for i in range(1000)]

# WithClassVar: ~1KB for class variable + 1000 * (small overhead)
# WithInstanceVar: 1000 * (~1KB + small overhead)

Use class variables for truly shared, immutable data like configuration constants, enumerations, or class-level metadata.

Inspection and Debugging

Python provides tools to inspect where variables are stored and distinguish between class and instance variables.

class Product:
    category = "General"
    
    def __init__(self, name, price):
        self.name = name
        self.price = price

product = Product("Widget", 29.99)

# Instance namespace
print(product.__dict__)  # {'name': 'Widget', 'price': 29.99}

# Class namespace
print(Product.__dict__['category'])  # General

# Check if attribute is in instance
print('name' in product.__dict__)      # True
print('category' in product.__dict__)  # False

# Get all instance variables
print(vars(product))  # {'name': 'Widget', 'price': 29.99}

Practical Pattern: Configuration with Override

A common pattern combines class variables for defaults with instance variables for overrides:

class Service:
    # Class-level defaults
    timeout = 30
    retry_count = 3
    base_url = "https://api.example.com"
    
    def __init__(self, name, **overrides):
        self.name = name
        
        # Apply instance-specific overrides
        for key, value in overrides.items():
            if hasattr(type(self), key):
                setattr(self, key, value)
    
    def get_config(self):
        return {
            'timeout': self.timeout,
            'retry_count': self.retry_count,
            'base_url': self.base_url
        }

# Use defaults
service1 = Service("primary")
print(service1.get_config())  
# {'timeout': 30, 'retry_count': 3, 'base_url': 'https://api.example.com'}

# Override specific values
service2 = Service("secondary", timeout=60, retry_count=5)
print(service2.get_config())
# {'timeout': 60, 'retry_count': 5, 'base_url': 'https://api.example.com'}

# Class defaults remain unchanged
print(Service.timeout)  # 30

This pattern provides sensible defaults while allowing instance-specific customization without modifying shared state.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.