SECTION 1
Historical Context & Motivation
Before object-oriented programming (OOP) became widespread, programmers stored data in loose collections of variables and functions. As software grew more complex in the 1960s and 1970s, developers needed a way to bundle data together with the behavior that operates on it. The concept of a class — a blueprint for creating objects — became the central organizing idea, and with it came a critical design question: should a piece of data belong to every object made from the class, or should it belong to the class blueprint itself?
1967 • Simula Introduces Classes
1980 • Smalltalk Refines the Model
1991 • Python Is Released
2000s • OOP Becomes Standard Practice
The question at the heart of this lesson is deceptively simple: when you create a variable inside a class, does it belong to the blueprint itself (so every object shares it), or does it belong to each individual object (so each one can have its own value)? Getting this right is fundamental to writing clean, bug-free Python code.
SECTION 2
Core Principles & Definitions
In Python, a class is a template or blueprint that describes what data and behavior its objects will have. When you call the class to create an object, you get an instance — a specific, concrete copy built from that template. Attributes are simply variables that store data, and the key distinction is where those variables are defined.
Class Attribute
Instance Attribute
__init__) using self.name = value. It is unique to each instance.Attribute Lookup Order
Mutability Matters
KEY TAKEAWAY
SECTION 3
Visual Explanation
In the diagram above, notice how species lives in the class namespace and is shared. When you write dog1.species, Python doesn't find species on the instance, so it travels up to the class to retrieve it. Meanwhile, self.name and self.age are created in __init__ and stored directly on each instance. This separation is what allows two Dog objects to share a species but have completely different names.
SECTION 4
How Attribute Lookup Works
Understanding attribute lookup is the key to mastering class and instance attributes. Every object in Python carries a hidden dictionary called __dict__ that holds its attributes. When you access obj.attribute, Python follows a specific search path.
The Lookup Chain
- Step 1: Python checks the instance's
__dict__for the attribute. - Step 2: If not found, Python checks the class's
__dict__. - Step 3: If still not found, Python checks parent classes (inheritance chain).
- Step 4: If nowhere is it found, Python raises an
AttributeError.
The Shadowing Effect
Here's where things get tricky. If you write dog1.species = "Wolf", Python does not modify the class attribute. Instead, it creates a new instance attribute called species on dog1 that shadows (hides) the class attribute. Now dog1.species returns "Wolf", but dog2.species still returns "Canis lupus" because dog2 still falls through to the class.
⚠️ MUTABLE CLASS ATTRIBUTE TRAP
dog1.tricks.append("roll over") modifies the shared list in place. This means dog2.tricks will also show "roll over"! This is one of the most common bugs for Python beginners. Always define mutable data as instance attributes in __init__.SECTION 5
Side-by-Side Comparison
| Feature | Class Attribute | Instance Attribute |
|---|---|---|
| Defined where? | Directly in class body | Inside __init__ using self |
| Scope | Shared across all instances | Unique to each instance |
| Memory | One copy in class __dict__ | One copy per instance __dict__ |
| Use case | Constants, counters, defaults | Object-specific state |
| Mutable types safe? | Risky — changes affect all instances | Safe — each instance gets its own copy |
Quick-reference comparison of class attributes vs. instance attributes
SECTION 6
Worked Example — Building a Student Class
Let's build a Student class that tracks the school name (shared by all students) and each student's individual name and GPA. We'll walk through every line and explain where each attribute lives.
Creating a Student Class with Both Attribute Types
- 1
Step 1 — Define the class with a class attribute
We defineschooldirectly in the class body. Every student shares this value.class Student: school = "Lincoln High" total_students = 0school and total_students are class attributes, stored in Student.__dict__ - 2
Step 2 — Define __init__ with instance attributes
Inside__init__, we useselfto attach unique data to each instance. We also increment the class counter.def __init__(self, name, gpa): self.name = name self.gpa = gpa Student.total_students += 1self.name and self.gpa are instance attributes; total_students is updated via the class name - 3
Step 3 — Create instances and inspect
We create two students and check what each one sees.s1 = Student("Alice", 3.9) s2 = Student("Bob", 3.5)Nows1.schoolreturns "Lincoln High" (found in class),s1.namereturns "Alice" (found in instance), andStudent.total_studentsreturns 2.s1.school → "Lincoln High" | s1.name → "Alice" | Student.total_students → 2 - 4
Step 4 — Observe the shadowing effect
What happens if we assign a school to just one student?s1.school = "Jefferson Academy"This does NOT change the class attribute. Instead, Python creates a new instance attribute on s1 that shadows the class attribute. Nows1.schoolreturns "Jefferson Academy", buts2.schoolstill returns "Lincoln High".s1.school → "Jefferson Academy" (instance) | s2.school → "Lincoln High" (class) | Student.school → "Lincoln High"
TIP: Modifying Class Attributes
Student.school = "New Name". Using self.school = ... or s1.school = ... will only create a local instance attribute that shadows the class one.SECTION 7
When to Use Class vs. Instance Attributes
Choosing the right type of attribute is a design decision. There are clear situations where each type is the best choice, and mixing them up can lead to confusing bugs or poor code organization.
| Scenario | Use Class Attribute? | Use Instance Attribute? |
|---|---|---|
| A value that should be the same for every object (e.g., pi for a Circle class) | ✅ Yes — define once, share everywhere | ❌ No — wastes memory repeating the same value |
| A counter tracking how many instances have been created | ✅ Yes — it describes the class, not one object | ❌ No — no single instance "owns" this count |
| A user's name, email, or score | ❌ No — each user is different | ✅ Yes — unique to each object |
| A list of items (e.g., a shopping cart) | ❌ Dangerous — mutable class attributes are shared! | ✅ Yes — create a new list in __init__ |
| A default configuration value that might be overridden | ✅ Works as a fallback default | ✅ Can shadow the default per instance |
Decision guide for choosing between class and instance attributes
KEY TAKEAWAY
SECTION 8
Connections to Advanced OOP Concepts
Understanding class and instance attributes lays the groundwork for several more advanced Python and OOP topics you'll encounter as you continue learning. The distinction between "belongs to the class" and "belongs to the instance" recurs throughout object-oriented design.
| This Lesson's Concept | Advanced Extension |
|---|---|
| Class attribute (shared data) | Class methods — use @classmethod decorator to create methods that operate on class attributes instead of instance data |
| Instance attribute (per-object data) | Properties — use @property to add validation and computed values when getting or setting instance attributes |
| Attribute lookup chain | Inheritance — child classes add another level to the lookup chain; the MRO determines which class's attribute is found first |
| Shadowing a class attribute | Descriptors — Python's descriptor protocol (__get__, __set__) gives you fine-grained control over how attribute access is customized |
| Mutable class attribute trap | Slots — using __slots__ restricts which instance attributes can exist, saving memory and preventing accidental attribute creation |
How today's concepts connect to advanced OOP topics
As you progress, you'll see that the class-vs-instance pattern applies to methods too: regular methods receive self and operate on instance data, class methods receive cls and operate on class data, and static methods receive neither. Mastering attributes now makes these advanced ideas much easier to grasp.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Car class.Show answer
wheels = 4 makes sense as a class attribute because (nearly) all cars have four wheels. However, self.color should be an instance attribute because each car can be a different color.Question 2: BASIC
print(p2.school) output?
class Person:
school = "Westfield"
def __init__(self, name):
self.name = name
p1 = Person("Ana")
p2 = Person("Leo")
p1.school = "Eastside"Show answer
p1.school = "Eastside", Python creates a new instance attribute on p1 that shadows the class attribute. This does not change the class attribute itself. Since p2 has no instance attribute called school, Python's lookup chain goes to the class and finds "Westfield". The key rule is: assigning via an instance creates an instance attribute; it never modifies the class attribute.Question 3: INTERMEDIATE
print output, and why?
class Team:
members = []
def __init__(self, name):
self.name = name
Team.members.append(name)
t1 = Team("Alice")
t2 = Team("Bob")
print(t1.members)
print(t2.members)Show answer
members is a class attribute, meaning there is only one list object shared across all instances. When we call Team.members.append(name), we're modifying that single shared list in place. Both t1 and t2 point to the same list, so both see all the names. If each team should have its own member list, members should be initialized as self.members = [] inside __init__.Question 4: APPLIED
BankAccount class. The interest rate is 2.5% for all accounts, each account has its own owner name and balance, and you want to track the total number of accounts created. Write the class definition, identifying which data should be a class attribute and which should be an instance attribute. Explain your reasoning for each.Show answer
class BankAccount:
interest_rate = 0.025
total_accounts = 0
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
BankAccount.total_accounts += 1
The interest_rate is a class attribute because the bank sets one rate for all accounts — it would be wasteful and error-prone to store 0.025 on every instance. Similarly, total_accounts is a class attribute because it describes the class as a whole, not any single account. The owner and balance are instance attributes because they are unique to each account — Alice's balance shouldn't change when Bob deposits money. We increment total_accounts using BankAccount.total_accounts += 1 (the class name) to ensure we modify the class attribute, not accidentally create an instance attribute.Question 5: CRITICAL THINKING
Show answer
species = "Canis lupus" at the class level, they immediately understand this is a shared constant, not something that varies per dog. Design choices communicate meaning to other programmers, and choosing the right attribute type is part of writing readable, maintainable code.SUMMARY
Lesson Summary
Summary
In Python, class attributes are variables defined directly in the class body and are shared by all instances, making them ideal for constants, counters, and default values. In contrast, instance attributes are defined inside __init__ using self and are unique to each object, perfect for storing data that differs from one instance to another.
Python's attribute lookup chain checks the instance first, then the class, then parent classes. Assigning to an instance can shadow a class attribute without changing it. Be especially careful with mutable class attributes like lists — modifying them in place affects all instances. When in doubt, make it an instance attribute in __init__.