Previous | Tutorial index | Next

📘 TUTORIAL 6: CLASS ATTRIBUTES VS. INSTANCE ATTRIBUTES

Learning Objective

Distinguish between attributes shared by all instances and attributes unique to each instance.

6.1 Two Types of Attributes in Python Classes

When you define a class, you can create two fundamentally different kinds of attributes:

  1. Instance Attributes – belong to individual instances. Each object has its own copy. They are typically defined inside __init__ using self.attribute = value.
  2. Class Attributes – belong to the class itself. All instances share the same value. They are defined directly in the class body, outside any method.

Quick Example

class Dog: # Class attribute – shared by all dogs species = "Canis familiaris" def __init__(self, name): # Instance attribute – unique to each dog self.name = name # Creating instances fido = Dog("Fido") rex = Dog("Rex") # Accessing instance attributes (unique) print(fido.name) # Fido print(rex.name) # Rex # Accessing class attribute (shared) print(fido.species) # Canis familiaris print(rex.species) # Canis familiaris print(Dog.species) # Canis familiaris (access via class)

Both instances see the same species value. This is the essence of class attributes.

6.2 Where Are Class Attributes Stored?

This separation is why class attributes are shared—all instances point to the same class object, and when you look up an attribute, Python checks the instance first, then the class.

Visualizing the __dict__

class Dog: species = "Canis familiaris" def __init__(self, name): self.name = name fido = Dog("Fido") print(fido.__dict__) # {'name': 'Fido'} print(Dog.__dict__) # Contains 'species' and other class stuff

The instance dictionary only contains name; species is in the class dictionary.

6.3 Accessing Class Attributes

Class attributes can be accessed in two ways:

  1. Through the classClassName.attribute
  2. Through an instanceinstance.attribute

Both will resolve to the same value.

class Counter: count = 0 # Class attribute print(Counter.count) # 0 – via class c1 = Counter() c2 = Counter() print(c1.count) # 0 – via instance print(c2.count) # 0 – via instance

This works because when Python encounters c1.count, it first looks for count in the instance's __dict__. If not found, it looks in the class's __dict__.

6.4 The Crucial Distinction: Modifying Class Attributes

Important Rule: When you assign a value to an attribute using an instance (e.g., c1.count = 5), you are creating a new instance attribute that shadows the class attribute. The class attribute remains unchanged.

Demonstration

class Counter: count = 0 # Class attribute c1 = Counter() c2 = Counter() print(c1.count) # 0 – uses class attribute print(c2.count) # 0 – uses class attribute c1.count = 5 # Creates an instance attribute 'count' in c1 print(c1.count) # 5 – uses instance attribute (shadows class) print(c2.count) # 0 – still uses class attribute print(Counter.count) # 0 – class attribute unchanged

What happened:

Modifying the Class Attribute Properly

To change the class attribute for all instances, modify it through the class itself:

Counter.count = 10 print(c1.count) # 5 – still the instance attribute (shadows) print(c2.count) # 10 – now the class attribute is updated print(Counter.count) # 10

Key Insight: If any instance has a shadowing instance attribute, it will not see the updated class attribute. This can be a source of subtle bugs.

6.5 Mutating Class Attributes (e.g., Lists, Dictionaries)

The behaviour becomes even more nuanced with mutable objects. If a class attribute is a mutable object (like a list or dictionary), modifying the object in‑place (e.g., append, extend) does not create a new instance attribute—it modifies the shared object.

Example with a List

class Team: members = [] # Class attribute – shared list team1 = Team() team2 = Team() team1.members.append("Alice") # Modifies the shared list in‑place print(team2.members) # ['Alice'] – team2 sees the change print(Team.members) # ['Alice'] – class sees it too

But, if you assign to team1.members = [...], you create a new instance attribute that shadows the class attribute.

team1.members = ["Bob"] # Creates instance attribute, shadows class team2.members.append("Charlie") # Modifies the shared list (since team2 has no shadow) print(team1.members) # ['Bob'] – instance attribute print(team2.members) # ['Alice', 'Charlie'] – class attribute (modified) print(Team.members) # ['Alice', 'Charlie'] – class attribute

Best Practice: If you intend to share a mutable object among all instances, be very careful with in‑place modifications. It's often safer to treat the class attribute as a constant (immutable) or to use methods that clearly indicate they are modifying shared state.

6.6 Common Use Cases for Class Attributes

Class attributes are useful for:

  1. Constants – values that are the same for all instances, like MAX_SPEED or DEFAULT_COLOR.
  2. Default values – a default that can be overridden per instance if needed.
  3. Shared counters – tracking how many instances have been created.
  4. Configuration settings – that apply to all objects of that class.
  5. Caching – storing shared data that all instances can use.

Example: Instance Counter

class Student: count = 0 # Class attribute to count instances def __init__(self, name): self.name = name Student.count += 1 # Increment class attribute s1 = Student("Alice") s2 = Student("Bob") s3 = Student("Charlie") print(Student.count) # 3

Example: Default Value

class Circle: DEFAULT_RADIUS = 1 # Class constant def __init__(self, radius=None): if radius is None: self.radius = Circle.DEFAULT_RADIUS else: self.radius = radius

6.7 Python's Attribute Lookup Order (The MRO for Attributes)

When you access an attribute on an instance, Python searches in this order:

  1. Instance's __dict__ – the object's own attributes.
  2. Class's __dict__ – the class attributes.
  3. Superclass's __dict__ – following the Method Resolution Order (MRO) for inheritance.

This is why instance attributes take precedence over class attributes.

Example with Inheritance

class Animal: kingdom = "Animalia" class Mammal(Animal): kingdom = "Mammalia" # Override class attribute class Dog(Mammal): pass fido = Dog() print(fido.kingdom) # Mammalia (inherited from Mammal, not Animal)

If an instance has its own kingdom, it would override even the class's version.

6.8 Common Pitfalls with Class Attributes

Pitfall Explanation How to Avoid
Assigning to a class attribute via instance instance.attr = value creates a new instance attribute instead of modifying the class attribute. Always modify class attributes through the class: Class.attr = value.
Mutating mutable class attributes in‑place In‑place modifications (e.g., list.append) affect all instances, which may be unexpected. Document that the attribute is shared; consider making it immutable (tuple) or using a method to control changes.
Assuming class attributes are instance attributes If you don't override, they work, but if you later assign a value to one instance, the others still see the class value. Be explicit: use ClassName.attribute when you mean the class version to avoid confusion.
Using mutable class attributes as default values for methods This is a classic bug: def __init__(self, items=[]): creates a shared list across instances. Use None as default and create a new list inside the method.
Shadowing class attributes unintentionally If you assign to self.attr without realizing attr is a class attribute, you create a shadow. Use clear naming (e.g., class attributes in all caps) to distinguish them.

6.9 When to Use Class Attributes vs. Instance Attributes

Use Case Class Attribute Instance Attribute
Constants ✅ Yes – e.g., PI = 3.14159 ❌ Not needed
Default values ✅ Yes – if most instances use the same default. ✅ If each instance may need a different default, but you can still default to class attribute.
Counter (number of instances) ✅ Yes – shared across all instances. ❌ Would be per instance, useless for counting.
Data that varies per object ❌ No – use instance attributes. ✅ Yes – e.g., name, age, salary.
Configuration that should apply globally ✅ Yes – e.g., DEBUG = True. ❌ No – would be per instance, not global.
Cached data shared by all instances ✅ Yes – e.g., a cache dictionary. ❌ Would defeat the purpose of sharing.

6.10 Full Walkthrough Example: An Employee Class with Shared Settings

class Employee: # Class attributes – shared settings company = "TechCorp" default_salary = 50000 total_employees = 0 def __init__(self, name, salary=None): self.name = name # If no salary provided, use the class default if salary is None: self.salary = Employee.default_salary else: self.salary = salary Employee.total_employees += 1 def display(self): return f"{self.name} works at {Employee.company}, salary: ${self.salary}" # Create employees e1 = Employee("Alice") e2 = Employee("Bob", 60000) e3 = Employee("Charlie") print(e1.display()) # Alice works at TechCorp, salary: $50000 print(e2.display()) # Bob works at TechCorp, salary: $60000 print(e3.display()) # Charlie works at TechCorp, salary: $50000 print(Employee.total_employees) # 3 # Change the company name (affects all existing and future employees) Employee.company = "GlobalTech" print(e1.display()) # Alice works at GlobalTech, salary: $50000 print(e2.display()) # Bob works at GlobalTech, salary: $60000 # Change default salary for future employees Employee.default_salary = 55000 e4 = Employee("Diana") # Uses new default print(e4.display()) # Diana works at GlobalTech, salary: $55000

Observations:


📝 Quiz 6: Class Attributes vs. Instance Attributes

Answer the following questions to check your understanding.

1. How do you define a class attribute in Python?

Answer(B) `attribute = value` at the top level of the class body

2. What happens when you assign a value to an attribute using an instance (e.g., obj.attr = 5)?

Answer(B) A new instance attribute is created, shadowing the class attribute.

3. Consider the following code:

class Cat: sound = "meow" c1 = Cat() c2 = Cat() c1.sound = "purr" print(c2.sound)

What is the output?

Answer(A) `meow`

4. If you want to change a class attribute for all instances, you should:

Answer(B) Modify it through the class: `ClassName.attr = value`.

5. (True/False) In‑place modification of a mutable class attribute (e.g., list.append) creates a new instance attribute.

Answer(B) False – it modifies the existing shared object.

6. What is the correct way to define a default value for an instance attribute that should be shared unless overridden?

Answer(B) Define it as a class attribute and use it in `__init__` as a fallback.

7. Given the following code, what does print(Counter.count) output after the operations?

class Counter: count = 0 c1 = Counter() c2 = Counter() c1.count += 1 c2.count += 1 print(Counter.count)
Answer(A) `0` – the `+=` creates instance attributes, so the class attribute remains 0.

8. Class attributes are stored in:

Answer(B) The class's `__dict__`

🧪 Exercise 6: Working with Class and Instance Attributes

Part A: Create a Product Class
Define a class Product with:

Part B: Test the Class

  1. Create three product objects with different names and prices.
  2. Print Product.total_products – should be 3.
  3. Print the category of one product via the instance – should be "General".
  4. Change the category class attribute to "Electronics".
  5. Print the category of all products – they should all now be "Electronics".
  6. For one product, assign a new category via the instance (e.g., p1.category = "Food"). Print that product's category and another product's category – show the difference.

Part C: Mutable Class Attribute
Add a class attribute all_products as an empty list. In __init__, append each product's name to all_products. Test it by creating a few products and printing Product.all_products. Explain what happens if you later assign p1.all_products = [] – why is this different from in‑place modification?

Part D: Reflection
In comments, answer: "Why is total_products a class attribute rather than an instance attribute? What would happen if it were an instance attribute?"

Sample Answer `total_products` is a class attribute because it needs to be shared across all instances to keep a global count of how many products have been created. If it were an instance attribute, each product would have its own `total_products` counter, which would not reflect the total number of products. It would only count that specific instance and would be inconsistent across objects.

🏠 Homework 6: Building a Library Catalog System

Task: You are building a catalog system for a library. Create a complete Python script that models LibraryBook with both class and instance attributes to manage shared and per‑book data.

Class: LibraryBook

Class Attributes:

Instance Attributes (set in __init__):

Methods:

Additional Requirements:

Homework Submission Requirements

Part 1: Code
Write the complete LibraryBook class with all methods, proper use of class and instance attributes, and comments explaining your choices.

Part 2: Testing Script
In the same file, create a test function that does the following:

  1. Create at least 5 LibraryBook objects with various titles, authors, ISBNs, and genres (include some duplicate genres).
  2. Display the initial state: print total_books, available_books, and genre_categories.
  3. Check out two books (call check_out()), and display available_books after each.
  4. Return one of them, and display available_books again.
  5. For a checked‑out book, calculate a fine for 3 days late, print the fine amount.
  6. Change the library name via the class attribute LibraryBook.library_name = "Downtown Library".
  7. Print the display_info() for all books to show that the library name appears (you can include the library name in the display string).
  8. Attempt to check out a book that is already checked out – show that it returns False.
  9. Print genre_categories to see the unique genres collected.

Part 3: Reflection Questions
Answer these in a comment block at the top of your script:

  1. Why is total_books a class attribute? What would be the problem if it were an instance attribute?
  2. Why did we use a class attribute available_books instead of computing it by iterating over all books each time?
  3. When we assigned p1.genre_categories = ["NewGenre"], what would happen to the class attribute? Explain the shadowing behaviour.
  4. The calculate_fine() method uses LibraryBook.default_fine_per_day. Why not use self.default_fine_per_day? What if we wanted to allow per‑book fine rates?
  5. What is the advantage of collecting all unique genres in a class attribute? How would you use this list elsewhere in the system?
Sample Answers (Part 3) 1. `total_books` must be a class attribute because it needs to reflect the total number of book objects created across the entire system. If it were an instance attribute, each book would have its own count, which would always be 1 and would not give the overall total. 2. Using a class attribute `available_books` allows O(1) access to the count without having to iterate over all book instances each time. This is more efficient, especially when the library contains thousands of books. 3. Assigning `p1.genre_categories = ["NewGenre"]` would create a new instance attribute on `p1` that shadows the class attribute. The class attribute `genre_categories` would remain unchanged, and other book instances would still see the original list. 4. `calculate_fine()` uses the class attribute to provide a global default fine rate. If we wanted per‑book fine rates, we could add an instance attribute `self.fine_rate` and then use `self.fine_rate` if present, falling back to the class default. 5. Collecting unique genres in a class attribute provides a centralised list of all genres used in the library. This could be used for filtering books by genre, generating a genre list in a user interface, or enforcing that new books only use existing genres.

📚 Additional Resources for Self‑Study

  1. Educative: Understanding Python Class and Instance Attributes – Clear explanation with examples.
  2. CC 210 Textbook: Attributes & Initialization – Academic perspective.
  3. Real Python: Instance, Class, and Static Methods (covers attributes) – Though focused on methods, the attribute sections are helpful.
  4. Python Official Docs: 9. Classes – Class and Instance Variables – The authoritative source.

✅ Summary Checklist for Tutorial 6

Before moving to Tutorial 7 (Class and Static Methods), ensure you can confidently say YES to the following:

Ready for the next tutorial? In Tutorial 7, you will learn about class methods and static methods – how to define methods that operate at the class level rather than on instances, and when to use each.

Previous | Tutorial index | Next