Class and Instance Attributes, Methods, and the `__init__` Method
From the Classes(python) curriculum
TL;DR
You'll learn how Python classes define blueprints for objects. We'll cover how objects store unique data in instance attributes and share behavior through methods. Understanding the __init__ method is key to setting up new objects correctly.
1. The Mental Model
Think of a class as a cookie cutter: it defines the shape and features, but isn't a cookie itself. An instance is an actual cookie, created by the cutter, each with its own frosting and sprinkles. The __init__ method is like the first step after cutting: it sets up that specific cookie. Every cookie (instance) will have the same basic shape (class attributes) and can be decorated (methods), but its unique details (instance attributes) are its own.
2. The Core Material
When you write a class in Python, you're essentially creating a blueprint. From this blueprint, you can then create individual objects, known as instances. These instances are what you actually work with in your code.
Class Attributes: Shared Properties

Photo by Kampus Production on Pexels
Class attributes are variables that belong to the class itself, not to any specific instance of the class. This means they are shared by all instances of that class. If you change a class attribute, that change will be reflected across all instances (unless an instance has overridden it with an instance attribute of the same name, which we'll get to). They're great for storing data that's constant for all objects of a certain type, or for configuration settings.
class Dog:
# This is a class attribute, shared by all Dog instances
species = "Canis familiaris"
number_of_legs = 4
dog1 = Dog()
dog2 = Dog()
print(f"Dog 1 species: {dog1.species}")
print(f"Dog 2 species: {dog2.species}")
# You can also access it directly from the class
print(f"Class species: {Dog.species}")
Dog.number_of_legs = 3 # Oh no, a tripod dog!
print(f"Dog 1 legs: {dog1.number_of_legs}")
print(f"Dog 2 legs: {dog2.number_of_legs}")
Notice how changing Dog.number_of_legs immediately affected both dog1 and dog2 because they share this attribute.
Instance Attributes: Unique Data

Photo by Markus Winkler on Pexels
Instance attributes are variables that belong to a specific instance of a class. Each instance has its own copy of these attributes, and their values can differ from one instance to another. They are typically defined within the __init__ method.
The __init__ Method: Object Constructor
The __init__ method is a special method in Python classes. It's often called the "constructor," though technically it's an initializer. When you create a new instance of a class, __init__ is automatically called. Its purpose is to set up the initial state of the new object by assigning values to its instance attributes.
The first parameter of __init__ (and all instance methods) is conventionally named self. self is a reference to the instance itself. When you write self.attribute_name = value inside __init__, you're creating an instance attribute unique to that particular object.
class Car:
# Class attribute
wheels = 4
def __init__(self, make, model, year):
# Instance attributes, unique to each Car object
self.make = make
self.model = model
self.year = year
self.mileage = 0 # Default instance attribute
car1 = Car("Toyota", "Camry", 2020)
car2 = Car("Honda", "Civic", 2022)
print(f"Car 1: {car1.make} {car1.model}, Year: {car1.year}, Wheels: {car1.wheels}, Mileage: {car1.mileage}")
print(f"Car 2: {car2.make} {car2.model}, Year: {car2.year}, Wheels: {car2.wheels}, Mileage: {car2.mileage}")
# Change mileage for car1 only
car1.mileage = 15000
print(f"Car 1 mileage after update: {car1.mileage}")
print(f"Car 2 mileage (unchanged): {car2.mileage}")
As you can see, car1.mileage changed, but car2.mileage did not. This is because mileage is an instance attribute.
Methods: Object Behavior
Methods are functions that belong to a class and define the behaviors or actions that objects of that class can perform. Just like __init__, the first parameter of an instance method is always self, allowing the method to access and modify the instance's attributes.
class Dog:
species = "Canis familiaris"
def __init__(self, name, breed):
self.name = name
self.breed = breed
self.is_sitting = False # Default state
# Instance method
def bark(self):
return f"{self.name} says Woof!"
# Instance method
def sit(self):
if not self.is_sitting:
self.is_sitting = True
return f"{self.name} is now sitting."
return f"{self.name} is already sitting."
# Instance method
def stand(self):
if self.is_sitting:
self.is_sitting = False
return f"{self.name} stands up."
return f"{self.name} is already standing."
fido = Dog("Fido", "Golden Retriever")
buddy = Dog("Buddy", "Labrador")
print(fido.bark())
print(buddy.bark())
print(fido.sit())
print(fido.is_sitting)
print(fido.sit()) # Tries to sit again
print(fido.stand())
print(fido.is_sitting)
print(f"Buddy is sitting: {buddy.is_sitting}") # Buddy's state is independent
Here's a diagram showing the flow of creating an instance and how attributes and methods fit in:
graph TD
A[Define Class `Dog`] --> B{Has `__init__` method?};
B -- Yes --> C[Define Class Attributes (e.g., `species`)];
C --> D[Define Instance Attributes (in `__init__`)];
D --> E[Define Instance Methods (e.g., `bark`, `sit`)];
E --> F[Create Instance: `my_dog = Dog("Fido", "Golden")`];
F --> G[`__init__` method automatically called];
G --> H[Assign `self.name`, `self.breed`];
H --> I[Instance `my_dog` created];
I --> J[Access Instance Attributes: `my_dog.name`];
I --> K[Call Instance Methods: `my_dog.bark()`];
K --> L[Method accesses `self.name`];
I --> M[Access Class Attributes: `my_dog.species`];
M --> N[All `Dog` instances share `species`];
3. Worked Example
Let's put this all together by creating a BankAccount class. We'll include class attributes for bank-wide policies, instance attributes for unique account details, and methods for account operations.
import uuid # Used to generate unique account numbers
class BankAccount:
# Class attribute: shared by all bank accounts
bank_name = "Global Trust Bank"
interest_rate = 0.015 # 1.5% annual interest
def __init__(self, account_holder_name, initial_balance=0.0):
# Instance attributes: unique to each account
self.account_id = str(uuid.uuid4())[:8] # Generate a short unique ID
self.account_holder = account_holder_name
self.balance = initial_balance
# Basic validation for initial balance
if self.balance < 0:
print("Warning: Initial balance cannot be negative. Setting to 0.")
self.balance = 0.0
# Instance method: deposit money
def deposit(self, amount):
if amount > 0:
self.balance += amount
return f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}"
return "Deposit amount must be positive."
# Instance method: withdraw money
def withdraw(self, amount):
if amount <= 0:
return "Withdrawal amount must be positive."
if self.balance >= amount:
self.balance -= amount
return f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}"
return "Insufficient funds."
# Instance method: display account info
def get_account_info(self):
return (f"--- Account Details ---\n"
f"Bank: {BankAccount.bank_name}\n" # Access class attribute via class name
f"Account ID: {self.account_id}\n"
f"Holder: {self.account_holder}\n"
f"Balance: ${self.balance:.2f}\n"
f"Interest Rate: {self.interest_rate*100:.1f}%") # Access class attribute via self
# Create two bank accounts
account1 = BankAccount("Alice Wonderland", 1000.00)
account2 = BankAccount("Bob The Builder") # Default initial_balance is 0.0
print(account1.get_account_info())
print("\n" + account2.get_account_info())
# Perform some transactions
print("\n--- Transactions for Alice ---")
print(account1.deposit(200.50))
print(account1.withdraw(50.00))
print(account1.withdraw(1500.00)) # Insufficient funds
print(account1.deposit(-100)) # Invalid deposit
print("\n--- Transactions for Bob ---")
print(account2.deposit(500.00))
print(account2.withdraw(200.00))
print("\n--- Final Balances ---")
print(account1.get_account_info())
print("\n" + account2.get_account_info())
# Show shared class attribute
print(f"\nAll accounts are at {BankAccount.bank_name}.")
# What if the bank changes its name?
BankAccount.bank_name = "MegaCorp Financial"
print(f"After bank name change: {account1.get_account_info()}") # It updates for all!
Expected Output (IDs will differ slightly):
--- Account Details ---
Bank: Global Trust Bank
Account ID: d6b2c8a1
Holder: Alice Wonderland
Balance: $1000.00
Interest Rate: 1.5%
--- Account Details ---
Bank: Global Trust Bank
Account ID: f0a3b1d2
Holder: Bob The Builder
Balance: $0.00
Interest Rate: 1.5%
--- Transactions for Alice ---
Deposited $200.50. New balance: $1200.50
Withdrew $50.00. New balance: $1150.50
Insufficient funds.
Deposit amount must be positive.
--- Transactions for Bob ---
Deposited $500.00. New balance: $500.00
Withdrew $200.00. New balance: $300.00
--- Final Balances ---
--- Account Details ---
Bank: Global Trust Bank
Account ID: d6b2c8a1
Holder: Alice Wonderland
Balance: $1150.50
Interest Rate: 1.5%
--- Account Details ---
Bank: Global Trust Bank
Account ID: f0a3b1d2
Holder: Bob The Builder
Balance: $300.00
Interest Rate: 1.5%
All accounts are at Global Trust Bank.
After bank name change: --- Account Details ---
Bank: MegaCorp Financial
Account ID: d6b2c8a1
Holder: Alice Wonderland
Balance: $1150.50
Interest Rate: 1.5%
4. Production Pitfalls & Best Practices
4.1 Real-World Best Practices
- Use
__init__for all necessary instance state: Ensure every critical piece of data an object needs to function is set up in__init__. This makes objects predictable and reliable right after creation. - Explicit
selffor instance access: Always useself.attribute_nameto access or modify instance attributes within methods. It clearly distinguishes instance data from local variables. - Use Class Attributes for Shared Constants: If a value is the same for all instances and doesn't change much (e.g.,
PI,DATABASE_NAME, default settings), make it a class attribute. This avoids memory duplication and ensures consistency. - Descriptive Method Names: Choose clear, action-oriented names for your methods (e.g.,
calculate_area,send_email,process_payment). This makes your code easier to read and understand. - Parameter Validation in
__init__and Methods: Add checks to__init__parameters (likeinitial_balancein our example) and method arguments (likeamountindeposit) to ensure valid data. This prevents bad states and bugs.
4.2 Common Bugs & Anti-Patterns
-
Forgetting
selfin Method Definitions:
BAD CODE:
python class MyClass: def my_method(arg): # Missing self print(arg) # This will raise a TypeError when called, because Python automatically passes the instance as the first argument.
FIXED CODE:
python class MyClass: def my_method(self, arg): # Added self print(arg) -
Accidentally Overwriting Class Attributes with Instance Attributes:
BAD CODE:
```python
class Product:
category = "Electronics"def __init__(self, name): self.name = name def change_category(self, new_category): self.category = new_category # This creates an *instance* attribute # and hides the class attribute for *this* instanceitem1 = Product("Laptop")
item2 = Product("Mouse")
item1.change_category("Computers")print(f"Item 1 category: {item1.category}") # Prints 'Computers'
print(f"Item 2 category: {item2.category}") # Still prints 'Electronics'
print(f"Class category: {Product.category}") # Still prints 'Electronics'
*FIXED CODE (if you intend to change for all):*python
class Product:
category = "Electronics"def __init__(self, name): self.name = name def change_category_for_all(self, new_category): Product.category = new_category # Changes the class attribute directly # Affects all instances that haven't set their ownitem1 = Product("Laptop")
item2 = Product("Mouse")
item1.change_category_for_all("Computers")print(f"Item 1 category: {item1.category}") # Now prints 'Computers'
print(f"Item 2 category: {item2.category}") # Also prints 'Computers'
print(f"Class category: {Product.category}") # Also prints 'Computers'
```
(Note: if the intention was for only that specific instance to have a new category, the "bad code" was actually correct for that specific scenario. It depends on intent.) -
Modifying Mutable Class Attributes Directly: If a class attribute is a mutable object (like a list or dictionary), modifying it directly via an instance will affect all other instances.
BAD CODE:
```python
class Employee:
all_skills = [] # Mutable class attributedef __init__(self, name): self.name = name def add_skill(self, skill): self.all_skills.append(skill) # Modifies the class's list directly!emp1 = Employee("Alice")
emp2 = Employee("Bob")emp1.add_skill("Python")
print(f"Alice's skills: {emp1.all_skills}") # ['Python']
print(f"Bob's skills: {emp2.all_skills}") # ['Python'] - Oops!
*FIXED CODE:*python
class Employee:
def init(self, name):
self.name = name
self.skills = [] # Each employee gets their own listdef add_skill(self, skill): self.skills.append(skill)emp1 = Employee("Alice")
emp2 = Employee("Bob")emp1.add_skill("Python")
print(f"Alice's skills: {emp1.skills}") # ['Python']
print(f"Bob's skills: {emp2.skills}") # [] - Correct!
```
4.3 When to Use What (Decision Matrix)
| Situation | Use This | Why |
|---|---|---|
| Data unique to each object | Instance Attributes (self.name = value) |
Ensures each object maintains its own distinct state. |
| Data shared by all objects of a class | Class Attributes (ClassName.attribute = value) |
Memory efficient; provides a single source of truth for shared data. |
| Initial setup of an object | __init__ Method |
Automatically called upon object creation to set initial state. |
| Defining actions an object can perform | Instance Methods (def method(self, ...):) |
Provides behaviors that operate on the object's unique data (self). |
| Data that must be set when object is created | __init__ parameters |
Forces required information to be passed when creating an instance. |
5. Now Try It
Let's build a simple Student class.
Your Task:
1. Define a class called Student.
2. Give it a class attribute called school_name and set its value to "Python University".
3. Implement the __init__ method. It should accept name and student_id as parameters.
4. Inside __init__, initialize instance attributes self.name and self.student_id.
5. Add a default instance attribute self.courses as an empty list.
6. Create an instance method enroll_course that takes a course_name string as an argument and adds it to the self.courses list.
7. Create an instance method get_student_info that returns a formatted string containing the student's name, ID, school name, and a list of enrolled courses.
8. Create two Student instances, enroll them in some courses, and print their info using your method.
Success looks like: You have created two Student objects, enrolled them in courses, and can print out their unique details along with the shared school_name.
Frequently asked about Class and Instance Attributes, Methods, and the `__init__` Method
Study this next
Get the full Classes(python) curriculum
Clone the complete plan to your dashboard for unlimited AI-generated notes, practice quizzes, and a personalised revision schedule.
Save this course free