26.2 Instance Attributes vs Class Attributes
In Python, both instance attributes and class attributes are fundamental to object-oriented programming, but they serve distinct purposes and behave differently. Understanding their distinction is crucial for designing robust and predictable classes.
Definition and Basic Syntax
An instance attribute is a variable that belongs to a specific, individual object (an instance) of a class. Its value is unique to that instance. You typically define instance attributes inside the __init__ method using self.
A class attribute is a variable that belongs to the class itself. This means its value is shared among all instances of that class. You define class attributes directly within the class body, but outside of any instance methods.
class Spaceship:
# Class Attribute - shared by all spaceships
fuel_type = "dilithium crystals"
ship_count = 0
def __init__(self, name, registry):
# Instance Attributes - unique to each spaceship
self.name = name
self.registry = registry
# Modifying a class attribute to track instances
Spaceship.ship_count += 1
# Creating instances
enterprise = Spaceship("Enterprise", "NCC-1701")
discovery = Spaceship("Discovery", "NCC-1031")
# Accessing instance attributes
print(enterprise.name) # Output: Enterprise
print(discovery.registry) # Output: NCC-1031
# Accessing class attributes (via class or instance)
print(Spaceship.fuel_type) # Output: dilithium crystals
print(enterprise.fuel_type) # Output: dilithium crystals
print(discovery.fuel_type) # Output: dilithium crystals
print(f"Total ships: {Spaceship.ship_count}") # Output: Total ships: 2
The Namespace Lookup Chain: How Attribute Access Works
When you access an attribute on an instance (e.g., my_instance.attribute), Python follows a specific search order, which explains why both instance and class attributes are accessible from an instance:
- Instance Namespace: It first checks if the attribute exists within the object’s own
__dict__. - Class Namespace: If not found, it then checks the class (and its parent classes) for the attribute.
This is why enterprise.fuel_type returns the class attribute’s value. The attribute fuel_type is not found in the enterprise instance’s namespace, so Python finds it in the Spaceship class’s namespace. This lookup chain is fundamental to inheritance and method resolution as well.
Mutability and The Critical Pitfall
The behavior of class attributes becomes a major pitfall when they are mutable, such as lists or dictionaries. Because the attribute is shared, modifying it through one instance modifies it for the class and all other instances.
class BorgCollective:
# 🚨 DANGER: Mutable class attribute
shared_memory = []
def __init__(self, designation):
self.designation = designation
self.shared_memory.append(f"Assimilated by {designation}")
borg_one = BorgCollective("One of Twelve")
borg_two = BorgCollective("Two of Twelve")
# The 'shared_memory' is the same list for all instances
print(borg_one.shared_memory)
# Output: ['Assimilated by One of Twelve', 'Assimilated by Two of Twelve']
print(BorgCollective.shared_memory) # Same output
This behavior is often unintended and leads to bugs. It occurs because borg_one.shared_memory and borg_two.shared_memory are both references to the exact same list object BorgCollective.shared_memory.
Best Practices and Common Use Cases
To avoid the mutable trap, a key best practice is to initialize mutable objects as instance attributes within __init__, not as class attributes.
Common uses for class attributes:
- Constants: Defining values that are constant for all instances (e.g.,
MAX_SPEED = 100,FUEL_TYPE). - Tracking Class-Wide Information: As shown with
ship_count, to keep a count of all created instances or aggregate data. - Default Values: Providing a default value that can be overridden by instance attributes. This is safe if the default is immutable (e.g., integers, strings, tuples).
class Player:
# Good use of class attributes: Immutable constants and defaults
max_health = 100
game_mode = "Campaign"
def __init__(self, name):
self.name = name
# Safe: overriding a class attr with an instance attr
self.health = Player.max_health
# Safe: creating a mutable instance attribute
self.inventory = [] # Unique to each player
def take_damage(self, amount):
self.health -= amount
player1 = Player("Alice")
player2 = Player("Bob")
player1.inventory.append("sword") # Only affects player1
print(player2.inventory) # Output: []
player1.game_mode = "Multiplayer" # Creates an instance attr!
print(player1.game_mode) # Output: Multiplayer (instance attr)
print(player2.game_mode) # Output: Campaign (class attr)
print(Player.game_mode) # Output: Campaign (unchanged)
Notice in the last example that assigning to player1.game_mode did not change the class attribute. Instead, it created a new instance attribute called game_mode that shadowed the class attribute for player1 only. This is the correct and safe way for an instance to have a different value.
Summary of Key Differences
| Feature | Instance Attribute | Class Attribute |
|---|---|---|
| Definition Scope | Inside __init__ or other methods using self. | Directly inside the class body |
| Namespace | Belongs to the instance object (obj.__dict__) | Belongs to the class object (Class.__dict__) |
| Data Sharing | Unique to each instance | Shared across all instances |
| Access via Class | Not directly accessible (without an instance) | Directly accessible (Class.attr) |
| Access via Instance | Directly accessible (instance.attr) | Accessible if no instance attr shadows it |
| Mutability Pitfall | N/A (always independent) | Critical: Mutable objects are shared |
In conclusion, use instance attributes for data that is unique to an object’s state. Use class attributes judiciously for truly shared data, preferring immutable objects, and be hyper-aware of the potential for unintended side-effects with mutable class attributes.