Skip to Content
Course content

117: Instance Attributes vs Class Attributes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this a lot when I'm reviewing code from developers moving into Python: they treat variables defined at the top of a class as if they are just "default values" for every instance. They think that by putting a list or a dictionary there, they're giving each object its own starting point. It feels intuitive, but in Python, it's a recipe for a very frustrating afternoon of debugging.

The "Default Value" Trap

Let's look at a classic mistake. Imagine you're building a system to track employees and the projects they're working on. You might be tempted to write it like this:

class Employee:
    projects = []  # I'm just setting a default empty list here, right?

    def __init__(self, name):
        self.name = name

    def add_project(self, project_name):
        self.projects.append(project_name)

alice = Employee("Alice")
bob = Employee("Bob")

alice.add_project("Website Redesign")
print(f"Alice's projects: {alice.projects}") # ['Website Redesign']
print(f"Bob's projects: {bob.projects}")     # ['Website Redesign'] <-- Wait, what?

You'll notice that Bob suddenly has Alice's project. Why? Because projects = [] is a class attribute. It exists in one place—the class definition itself—and is shared by every single instance of that class. When you called self.projects.append(), you weren't modifying a list belonging to Alice; you were modifying the one list that the entire Employee class shares. I've seen this bug crash production systems because a developer thought they were updating a user's session, but they were actually updating a global state for every user on the site.

Ownership: The Class Namespace vs. The Instance Namespace

To get this right, you have to understand that Python looks for attributes in a specific order. When you ask for self.projects, Python first looks at the individual instance (the self). If it doesn't find it there, it looks "up" at the class definition.

In the example above, self.projects didn't exist on the instance, so Python found the class attribute and modified it. But here is where it gets weird: if you assign a value to self.projects, Python creates a new attribute on the instance that "shadows" the class attribute.

class Employee:
    company_name = "TechCorp" # Shared by everyone

    def __init__(self, name):
        self.name = name # Unique to each person

# Both refer to the class attribute
alice = Employee("Alice")
bob = Employee("Bob")
print(alice.company_name) # TechCorp
print(bob.company_name)   # TechCorp

# This creates an INSTANCE attribute for Alice only
alice.company_name = "Freelance"

print(alice.company_name) # Freelance (Instance attribute)
print(bob.company_name)   # TechCorp (Still using Class attribute)


The rule of thumb I follow is simple: if the data should be unique to the object (like a name, a balance, or a list of tasks), it must be defined inside __init__ using self. If the data is a constant or a shared state that every object should see and potentially share (like a tax rate or a global configuration), put it at the class level.

  • Class Attributes: Defined outside __init__. Shared by all instances. Great for constants or tracking global state (like a total count of objects created).
  • Instance Attributes: Defined inside __init__. Unique to each instance. Essential for the "state" of a specific object.



📋 Practical Task

Fixing the Galactic Cargo Manifest Bug

You've been handed a piece of code for a space freighter system. The lead engineer tried to use a class attribute to store the cargo_manifest for each ship, but now every ship in the fleet is sharing the same cargo! When the S.S. Enterprise loads "Dilithium Crystals", the Millennium Falcon suddenly has them too.

Your Task: Modify the Spaceship class so that each ship has its own independent cargo manifest, while keeping the galaxy_name as a shared class attribute (since all ships are in the same galaxy).

class Spaceship:
    galaxy_name = "Andromeda"
    cargo_manifest = [] # This is the bug!

    def __init__(self, ship_name):
        self.ship_name = ship_name

    def load_cargo(self, item):
        self.cargo_manifest.append(item)

# Test your fix here:
ship1 = Spaceship("Voyager")
ship2 = Spaceship("Discovery")

ship1.load_cargo("Medical Supplies")
ship2.load_cargo("Fuel Cells")

print(f"{ship1.ship_name} cargo: {ship1.cargo_manifest}") 
# Expected: ['Medical Supplies']

print(f"{ship2.ship_name} cargo: {ship2.cargo_manifest}") 
# Expected: ['Fuel Cells']
Rating
0 0

There are no comments for now.

to be the first to leave a comment.