Previous | Tutorial index | Next
Distinguish between attributes shared by all instances and attributes unique to each instance.
When you define a class, you can create two fundamentally different kinds of attributes:
__init__ using self.attribute = value.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.
__dict__ (a dictionary of attribute names and values).__dict__.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.
__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.
Class attributes can be accessed in two ways:
ClassName.attributeinstance.attributeBoth 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__.
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.
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:
c1.count = 5 did not modify the class attribute. It created a new entry in c1.__dict__ with key 'count' and value 5.c1.count, it finds the instance attribute first and stops searching, never reaching the class attribute.c2.count still resolves to the class attribute because c2 has no instance attribute 'count'.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.
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.
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.
Class attributes are useful for:
MAX_SPEED or DEFAULT_COLOR.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
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
When you access an attribute on an instance, Python searches in this order:
__dict__ – the object's own attributes.__dict__ – the class attributes.__dict__ – following the Method Resolution Order (MRO) for inheritance.This is why instance attributes take precedence over class attributes.
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.
| 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. |
| 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. |
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:
company and default_salary are shared class attributes. Changing them affects all instances (and future ones).name and salary are instance attributes – each employee has their own.Answer the following questions to check your understanding.
1. How do you define a class attribute in Python?
self.attribute = value inside __init__attribute = value at the top level of the class bodyclass.attribute = value inside a method@classattr decorator2. What happens when you assign a value to an attribute using an instance (e.g., obj.attr = 5)?
3. Consider the following code:
class Cat:
sound = "meow"
c1 = Cat()
c2 = Cat()
c1.sound = "purr"
print(c2.sound)
What is the output?
meowpurrAttributeErrorNone4. If you want to change a class attribute for all instances, you should:
obj1.attr = value, obj2.attr = value, ...ClassName.attr = value.__init__ method.5. (True/False) In‑place modification of a mutable class attribute (e.g., list.append) creates a new instance attribute.
6. What is the correct way to define a default value for an instance attribute that should be shared unless overridden?
__init__.__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)
012AttributeError8. Class attributes are stored in:
__dict____dict____class_attrs__ dictionaryPart A: Create a Product Class
Define a class Product with:
category set to "General".total_products initially 0.__init__ method that takes name and price as parameters and initializes instance attributes self.name and self.price. Increment total_products by 1.Part B: Test the Class
Product.total_products – should be 3.category of one product via the instance – should be "General".category class attribute to "Electronics".category of all products – they should all now be "Electronics".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?"
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.
LibraryBookClass Attributes:
library_name – string, default "Central Library".total_books – integer, initial 0, counts all books ever created.available_books – integer, initial 0, counts books that are currently not checked out.default_fine_per_day – float, default 0.25 (dollars per day late).genre_categories – list, initially empty, used to collect all unique genres from all books.Instance Attributes (set in __init__):
title (str)author (str)isbn (str)genre (str)is_checked_out (bool, default False)Methods:
__init__(title, author, isbn, genre) – initialise all instance attributes; increment total_books and available_books (since the book starts available); add the genre to genre_categories if it's not already in the list.check_out() – if the book is available, mark it as checked out, decrement available_books, and return True; otherwise, return False.return_book() – if the book is checked out, mark it as available, increment available_books, and return True; otherwise, return False.calculate_fine(days_late) – returns days_late * LibraryBook.default_fine_per_day.display_info() – returns a string with all book details and current status.Additional Requirements:
set_library_name(new_name) as a class method? (Not required, but you can implement it if you want.)genre_categories list should be a class attribute that collects all unique genres. When adding a new book, check if its genre is already in the list; if not, append it.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:
LibraryBook objects with various titles, authors, ISBNs, and genres (include some duplicate genres).total_books, available_books, and genre_categories.check_out()), and display available_books after each.available_books again.LibraryBook.library_name = "Downtown Library".display_info() for all books to show that the library name appears (you can include the library name in the display string).False.genre_categories to see the unique genres collected.Part 3: Reflection Questions
Answer these in a comment block at the top of your script:
total_books a class attribute? What would be the problem if it were an instance attribute?available_books instead of computing it by iterating over all books each time?p1.genre_categories = ["NewGenre"], what would happen to the class attribute? Explain the shadowing behaviour.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?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.