Python Object-Oriented Programming: Classes, Objects, __init__, and self
Lesson 20: Python Object-Oriented Programming — Classes, Objects, __init__, and self
Lesson Introduction
Welcome to one of the most important lessons in your Python journey! By the end of this lesson, you will understand Object-Oriented Programming (OOP) — the style of writing code that is used in virtually every professional Python project in the world.
You will learn:
- What OOP is and why it exists
- What a class is and how to create one
- What an object is and how to create one from a class
- What the
__init__()method does and why it is so important - What
selfmeans and how it connects everything together
This is a big idea, but we will break it down one small step at a time. No prior knowledge of OOP is needed. Let’s go!
Prerequisite Concepts
Before diving into OOP, make sure you are comfortable with the following ideas. If any of these are new to you, read through the short explanation provided.
Variables — A variable stores a value in memory. Example: name = "Alice" stores the text "Alice" with the label name.
Functions — A function is a block of reusable code. Example:
def say_hello():
print("Hello!")
Calling a function — Running a function by writing its name followed by parentheses: say_hello()
Passing arguments — Giving information to a function when you call it: greet("Alice")
If you feel confident with those ideas, you are ready to begin.
Part 1: What is Object-Oriented Programming (OOP)?
Why Does OOP Exist?
Imagine you are building a school management system. You need to keep track of hundreds of students. Each student has a name, age, grade, and scores. Without OOP, you might write something like this:
student1_name = "Amara"
student1_age = 14
student1_grade = "JSS2"
student2_name = "Emeka"
student2_age = 15
student2_grade = "JSS3"
This gets messy very quickly. What if you have 500 students? You would need 1,500 separate variables. Finding, changing, or using any student’s data becomes a nightmare.
OOP solves this problem by letting you create a single “template” (called a class) that describes what a student looks like, and then produce as many individual students (called objects) as you need — each with their own data stored neatly inside them.
Analogy: Think of a class as a cookie cutter and objects as the individual cookies made from that cutter. Every cookie has the same shape (from the same cutter), but each one can have different decorations (its own data).
What Does OOP Give You?
OOP provides a clear structure to programs. It makes code:
- Easier to maintain — you fix one class, and all objects benefit
- Easier to reuse — you use the same class in many places
- Easier to read — code mirrors how the real world works
OOP also follows the DRY principle — Don’t Repeat Yourself. Instead of writing the same logic over and over, you write it once inside a class and reuse it.
Real-world connection: Every modern software system uses OOP. When you use a banking app, each bank account is an object. When you shop online, each product in your cart is an object. When you play a video game, each character is an object.
Part 2: Classes and Objects
What is a Class?
A class is a blueprint or template. It defines what data an object will hold and what actions an object can perform.
Think of it this way:
| Class (Blueprint) | Objects (Real Things Made from It) |
|---|---|
Fruit |
Apple, Banana, Mango |
Car |
Toyota Camry, Honda Civic, Kia Rio |
Student |
Amara, Emeka, Fatima |
BankAccount |
Account #001, Account #002, Account #003 |
The class describes the category. The objects are the actual specific things.
How to Create a Class
In Python, you create a class using the keyword class, followed by the name of the class (starting with a capital letter by convention), and a colon :.
class MyClass:
x = 5
Let’s break this down line by line:
class— this keyword tells Python: “I am about to define a class”MyClass— this is the name we give to our class (capital letter is the convention):— the colon marks the beginning of the class bodyx = 5— this is a property (a variable that belongs to the class), with the value5
Expected Output: Nothing yet — defining a class doesn’t produce output. It just creates the blueprint.
What is an Object?
An object is a specific instance created from a class. It is the real “thing” built using the blueprint.
To create an object, you write the class name followed by parentheses (), just like calling a function:
class MyClass:
x = 5
p1 = MyClass() # p1 is now an object created from MyClass
print(p1.x) # access the property x of p1
Expected Output:
5
Let’s trace through what happened:
MyClass()— Python reads the class blueprint and creates a new objectp1 = MyClass()— we store that object in the variablep1p1.x— we access the propertyxthat belongs top1- The
.(dot) is the way we access properties and methods of an object
Thinking Prompt: What do you think would happen if you typed
print(p1.y)? There is noydefined in the class — Python would raise anAttributeError. Try it!
Creating Multiple Objects from the Same Class
One of the greatest powers of OOP is that you can create as many objects as you need from a single class. Each object is completely independent.
class MyClass:
x = 5
p1 = MyClass()
p2 = MyClass()
p3 = MyClass()
print(p1.x)
print(p2.x)
print(p3.x)
Expected Output:
5
5
5
All three objects have their own copy of x. Each object is independent — changing one does not change the others.
Deleting an Object
You can remove an object from memory using the del keyword:
class MyClass:
x = 5
p1 = MyClass()
print(p1.x) # works fine
del p1 # delete the object
# print(p1.x) # this would now cause an error: NameError
Expected Output:
5
After del p1, the variable p1 no longer exists. Trying to use it would raise a NameError.
The pass Statement in Classes
A class definition cannot be completely empty — Python will give you an error. But sometimes you want to create a class as a placeholder for later. Use pass to create a valid empty class:
class Person:
pass # pass means "do nothing, but this is valid"
pass is Python’s way of saying: “I know this block is empty. That’s intentional.” It prevents errors in empty code blocks.
Part 3: The __init__() Method
The Problem: Every Object Looks the Same
With what we know so far, every object from MyClass has x = 5. But in real life, different students have different names, different cars have different brands. How do we give each object its own unique data when we create it?
The answer is the __init__() method.
What is __init__()?
__init__() is a special method built into Python. The name has double underscores on both sides (called dunder — “double under”). It is sometimes called the constructor.
__init__() is called automatically every time you create a new object from a class. It runs immediately at the moment of object creation.
Analogy: Think of
__init__()as the birth certificate form that gets filled out automatically the moment a new object is “born”. Every time you create a newPersonobject, Python automatically runs__init__()and fills in that person’s name, age, and other details.
Why Use __init__()?
Without __init__(), you would have to set properties one by one after creating the object — slow, repetitive, and easy to forget:
# Without __init__() — the hard, repetitive way
class Person:
pass
p1 = Person()
p1.name = "Tobias"
p1.age = 25
print(p1.name)
print(p1.age)
Expected Output:
Tobias
25
This works, but imagine doing this for 100 students. And what if you forget to set the age for one of them? Errors everywhere.
With __init__(), you set everything in one clean step:
# With __init__() — the clean, professional way
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person("Emil", 36)
print(p1.name)
print(p1.age)
Expected Output:
Emil
36
Breaking Down the __init__() Code Line by Line
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __init__(self, name, age):— defines the__init__method. It takes three parameters:self(explained in the next section),name, andageself.name = name— stores the value passed asnameinto this object’s own property callednameself.age = age— stores the value passed asageinto this object’s own property calledage
p1 = Person("Emil", 36)
Person("Emil", 36)— creates a newPersonobject and automatically calls__init__()withname = "Emil"andage = 36p1— the new object is stored inp1
print(p1.name) # "Emil"
print(p1.age) # 36
p1.name goes to p1’s personal storage and retrieves the name we gave it.
Thinking Prompt: What happens if you write
p1 = Person("Emil")— leaving out the age? Python will give aTypeErrorbecause__init__()expects bothnameandage.
Creating Multiple Objects with Different Data
Now the power becomes clear:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person("Amara", 14)
p2 = Person("Emeka", 15)
p3 = Person("Fatima", 13)
print(p1.name, p1.age)
print(p2.name, p2.age)
print(p3.name, p3.age)
Expected Output:
Amara 14
Emeka 15
Fatima 13
Three students, each with their own individual data, all from the same class. No repetitive variable names. Clean, organised, professional.
Default Values in __init__()
You can give parameters default values so that if the caller doesn’t provide one, it falls back to the default:
class Person:
def __init__(self, name, age=18): # age defaults to 18
self.name = name
self.age = age
p1 = Person("Emil") # age not given, uses default 18
p2 = Person("Tobias", 25) # age given, uses 25
print(p1.name, p1.age)
print(p2.name, p2.age)
Expected Output:
Emil 18
Tobias 25
p1didn’t get an age, so it used the default18p2got25, which overrode the default
Thinking Prompt: What would happen if you switched the order and put
def __init__(self, age=18, name):? Python would raise aSyntaxError— parameters with defaults must always come after parameters without defaults.
Multiple Parameters in __init__()
__init__() can accept as many parameters as you need:
class Person:
def __init__(self, name, age, city, country):
self.name = name
self.age = age
self.city = city
self.country = country
p1 = Person("Linus", 30, "Lagos", "Nigeria")
print(p1.name)
print(p1.age)
print(p1.city)
print(p1.country)
Expected Output:
Linus
30
Lagos
Nigeria
Each piece of data is stored inside the object and accessible through the dot notation (p1.city, etc.).
Part 4: The self Parameter
What is self?
self is the first parameter you see in almost every method inside a class. It is a reference to the current object — the specific instance that the method is being called on.
In plain English: self means “this particular object right now”.
Analogy: Imagine a class is a job description for an employee. The job description says “The employee will introduce themselves by name.” But when an actual employee (an object) follows that description,
selfis that specific employee. When Amara introduces herself,selfis Amara. When Emeka introduces himself,selfis Emeka.
Why Does self Exist?
Python needs self to know which object’s data to use when a method is called. Without self, a method inside a class would have no way to access the properties of a specific object.
Here’s the point illustrated clearly:
class Person:
def __init__(self, name):
self.name = name # store name in THIS object
def printname(self):
print(self.name) # print THIS object's name
p1 = Person("Tobias")
p2 = Person("Linus")
p1.printname() # self refers to p1 here
p2.printname() # self refers to p2 here
Expected Output:
Tobias
Linus
When you call p1.printname(), Python automatically passes p1 as self. So self.name becomes p1.name which is "Tobias". When you call p2.printname(), self becomes p2, so self.name is "Linus".
This is how Python knows which object’s data to work with!
self Must Always Be First
self must always be the first parameter of any method defined inside a class. You cannot skip it or move it elsewhere.
class Person:
def __init__(self, name, age): # self is first
self.name = name
self.age = age
def greet(self): # self is first here too
print("Hello, my name is " + self.name)
p1 = Person("Emil", 25)
p1.greet()
Expected Output:
Hello, my name is Emil
Notice that when you call p1.greet(), you do NOT pass self yourself. Python fills it in automatically. But when you define the method, self must be there.
Using self to Access Properties
self lets you reach into the object and retrieve or modify any property:
class Car:
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
def display_info(self):
print(f"{self.year} {self.brand} {self.model}")
car1 = Car("Toyota", "Corolla", 2020)
car1.display_info()
Expected Output:
2020 Toyota Corolla
Inside display_info, self.year, self.brand, and self.model all refer to the specific car that called the method.
Using self to Call Other Methods
You can use self to call one method from inside another method of the same class:
class Person:
def __init__(self, name):
self.name = name
def greet(self):
return "Hello, " + self.name
def welcome(self):
message = self.greet() # calling greet() from welcome()
print(message + "! Welcome to our website.")
p1 = Person("Tobias")
p1.welcome()
Expected Output:
Hello, Tobias! Welcome to our website.
self.greet() calls the greet method on the same object (p1), which returns the greeting string, which is then used in welcome.
Can self Be Named Something Else?
Technically, yes — self is just a convention. Python does not enforce the name. You could use any valid variable name:
class Person:
def __init__(myobject, name, age): # using "myobject" instead
myobject.name = name
myobject.age = age
def greet(abc): # using "abc" instead
print("Hello, my name is " + abc.name)
p1 = Person("Emil", 36)
p1.greet()
Expected Output:
Hello, my name is Emil
This works exactly the same. BUT — never actually do this in real code. Using self is a universal Python convention. Every Python programmer expects to see self. Using something else makes your code confusing and harder for others (and your future self) to read. Always use self.
Guided Practice Exercises
Exercise 1 — Warm-Up: Build a Simple Dog Class
Objective: Practice creating a class and objects with __init__().
Scenario: You are building a pet registration system. Create a Dog class.
Steps:
- Create a class called
Dog - Add
__init__()that acceptsnameandbreed - Store both values using
self - Create two Dog objects with different names and breeds
- Print both dogs’ names and breeds
Starter code:
class Dog:
def __init__(self, name, breed):
# your code here
dog1 = Dog("Rex", "German Shepherd")
dog2 = Dog("Bella", "Labrador")
# print dog1's name and breed
# print dog2's name and breed
Expected Output:
Rex German Shepherd
Bella Labrador
Solution:
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
dog1 = Dog("Rex", "German Shepherd")
dog2 = Dog("Bella", "Labrador")
print(dog1.name, dog1.breed)
print(dog2.name, dog2.breed)
Self-check Questions:
- What would happen if you forgot
selfin__init__? - What does
dog1.namemean — what is the.doing?
Exercise 2 — Adding a Method with self
Objective: Practice using self inside a method to access properties.
Scenario: Extend the Dog class with a method that makes the dog introduce itself.
Steps:
- Keep the
Dogclass from Exercise 1 - Add a method called
introduce()that prints:"Woof! My name is [name] and I am a [breed]." - Create two Dog objects and call
introduce()on both
Solution:
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def introduce(self):
print("Woof! My name is " + self.name + " and I am a " + self.breed + ".")
dog1 = Dog("Rex", "German Shepherd")
dog2 = Dog("Bella", "Labrador")
dog1.introduce()
dog2.introduce()
Expected Output:
Woof! My name is Rex and I am a German Shepherd.
Woof! My name is Bella and I am a Labrador.
What-if challenge: What if you add age to the class and print it in the introduce() method too?
Exercise 3 — Student Grade Tracker
Objective: Apply all concepts together in a realistic scenario.
Scenario: You are building a student result system for a school.
Requirements:
- A
Studentclass withname,subject, andscoreproperties - A method called
result()that prints:"[name] scored [score] in [subject]." - A method called
grade()that prints:"Grade: A"if score >= 70,"Grade: B"if score >= 50, otherwise"Grade: F"
Solution:
class Student:
def __init__(self, name, subject, score):
self.name = name
self.subject = subject
self.score = score
def result(self):
print(self.name + " scored " + str(self.score) + " in " + self.subject + ".")
def grade(self):
if self.score >= 70:
print("Grade: A")
elif self.score >= 50:
print("Grade: B")
else:
print("Grade: F")
s1 = Student("Amara", "Mathematics", 85)
s2 = Student("Emeka", "English", 55)
s3 = Student("Fatima", "Science", 42)
s1.result()
s1.grade()
s2.result()
s2.grade()
s3.result()
s3.grade()
Expected Output:
Amara scored 85 in Mathematics.
Grade: A
Emeka scored 55 in English.
Grade: B
Fatima scored 42 in Science.
Grade: F
Self-check Questions:
- Why do we write
str(self.score)instead of justself.scorein theresult()method? - What does
self.score >= 70compare? Whose score is it referring to?
Mini Project: Student Report Card System
Now let’s combine everything into a realistic mini-project.
Project Goal: Build a Student Report Card System that manages multiple students, tracks multiple subjects, and generates a report.
Stage 1 — Setup: Define the Student Class
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.scores = {} # dictionary to store subject → score pairs
def add_score(self, subject, score):
self.scores[subject] = score
def display_info(self):
print("Student Name: " + self.name)
print("Student ID: " + self.student_id)
Milestone 1 Test:
s1 = Student("Amara Okafor", "STU001")
s1.display_info()
Expected Output:
Student Name: Amara Okafor
Student ID: STU001
Stage 2 — Core Logic: Add Scores and Calculate Average
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.scores = {}
def add_score(self, subject, score):
self.scores[subject] = score
def calculate_average(self):
if len(self.scores) == 0:
return 0
total = sum(self.scores.values())
return total / len(self.scores)
def display_info(self):
print("Student Name: " + self.name)
print("Student ID: " + self.student_id)
Milestone 2 Test:
s1 = Student("Amara Okafor", "STU001")
s1.add_score("Mathematics", 88)
s1.add_score("English", 74)
s1.add_score("Science", 91)
average = s1.calculate_average()
print("Average score:", average)
Expected Output:
Average score: 84.33333333333333
Stage 3 — Enhancements: Full Report Generation
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.scores = {}
def add_score(self, subject, score):
self.scores[subject] = score
def calculate_average(self):
if len(self.scores) == 0:
return 0
total = sum(self.scores.values())
return total / len(self.scores)
def get_grade(self, average):
if average >= 70:
return "A — Excellent"
elif average >= 50:
return "B — Satisfactory"
else:
return "F — Needs Improvement"
def print_report(self):
print("=" * 40)
print("STUDENT REPORT CARD")
print("=" * 40)
print("Name: " + self.name)
print("Student ID: " + self.student_id)
print("-" * 40)
print("Subjects and Scores:")
for subject, score in self.scores.items():
print(" " + subject + ": " + str(score))
print("-" * 40)
avg = self.calculate_average()
print("Average Score: " + str(round(avg, 2)))
print("Overall Grade: " + self.get_grade(avg))
print("=" * 40)
Stage 4 — Final Output
# Create students
s1 = Student("Amara Okafor", "STU001")
s1.add_score("Mathematics", 88)
s1.add_score("English", 74)
s1.add_score("Science", 91)
s2 = Student("Emeka Nwosu", "STU002")
s2.add_score("Mathematics", 55)
s2.add_score("English", 63)
s2.add_score("Science", 48)
s3 = Student("Fatima Bello", "STU003")
s3.add_score("Mathematics", 35)
s3.add_score("English", 41)
s3.add_score("Science", 38)
# Print all reports
s1.print_report()
s2.print_report()
s3.print_report()
Expected Output:
========================================
STUDENT REPORT CARD
========================================
Name: Amara Okafor
Student ID: STU001
----------------------------------------
Subjects and Scores:
Mathematics: 88
English: 74
Science: 91
----------------------------------------
Average Score: 84.33
Overall Grade: A — Excellent
========================================
========================================
STUDENT REPORT CARD
========================================
Name: Emeka Nwosu
Student ID: STU002
----------------------------------------
Subjects and Scores:
Mathematics: 55
English: 63
Science: 48
----------------------------------------
Average Score: 55.33
Overall Grade: B — Satisfactory
========================================
========================================
STUDENT REPORT CARD
========================================
Name: Fatima Bello
Student ID: STU003
----------------------------------------
Subjects and Scores:
Mathematics: 35
English: 41
Science: 38
----------------------------------------
Average Score: 38.0
Overall Grade: F — Needs Improvement
========================================
Reflection Questions:
- How many lines would this have taken without OOP?
- What would happen if you wanted to add 50 more students?
- Can you add a method that checks if the student passed or failed?
Optional Extension: Add a highest_score() method that returns the subject where the student scored the most.
Common Beginner Mistakes
Mistake 1 — Forgetting self in a Method Definition
# WRONG
class Dog:
def __init__(name, breed): # missing self!
self.name = name
# RIGHT
class Dog:
def __init__(self, name, breed):
self.name = name
Python will raise a TypeError because without self, Python doesn’t know which object to assign the name to.
Mistake 2 — Passing self When Calling a Method
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(self.name + " says: Woof!")
dog1 = Dog("Rex")
dog1.bark(dog1) # WRONG — passing self manually
dog1.bark() # RIGHT — Python handles self automatically
Python fills in self automatically when you call a method on an object. Never pass it yourself.
Mistake 3 — Forgetting to Use self. When Accessing Properties
# WRONG
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(name + " says: Woof!") # 'name' doesn't exist here!
# RIGHT
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(self.name + " says: Woof!") # self.name is correct
Inside a method, you must use self.name to reach the object’s property. Just name by itself is a completely different (non-existent) variable inside the method’s local scope.
Mistake 4 — Leaving a Class Body Completely Empty
# WRONG
class Dog:
# no code here — syntax error!
# RIGHT
class Dog:
pass # use pass as a placeholder
Python requires at least something in a class body. pass is the correct placeholder.
Mistake 5 — Confusing the Class with the Object
class Dog:
def __init__(self, name):
self.name = name
# WRONG — accessing property on the class, not an object
print(Dog.name) # AttributeError!
# RIGHT — create an object first
dog1 = Dog("Rex")
print(dog1.name) # Rex
The class (Dog) is the blueprint. You must create an object (dog1) to have actual data.
Mistake 6 — Using a Different Name for self Without Understanding It
# This works but is confusing for everyone
class Dog:
def __init__(x, name):
x.name = name
This technically runs, but it confuses other programmers and your future self. Always use self.
Real-World Use Cases
OOP with classes is used everywhere in the real world:
Banking systems — Each bank account is a BankAccount object with properties like account_number, balance, and methods like deposit() and withdraw().
E-commerce — Each product is a Product object. Each customer is a Customer object. Each order is an Order object.
Games — Each game character is a Character object with properties like name, health, level, and methods like attack() and defend().
School management — Each student, teacher, and course is its own object — exactly like the project you just built.
Data science — Libraries like Pandas and NumPy are built entirely using classes. The DataFrame you create in Pandas is an object.
Reflection Questions
Take a moment to think through these questions before moving on:
- In your own words, what is the difference between a class and an object?
- What does
__init__()do, and when is it called? - Why is
selfthe first parameter in every method? - If you have two students
s1ands2created from the same class, and you calls1.greet(), how does Python know to prints1’s name and nots2’s? - Why is OOP better than creating separate variables for each piece of data?
- What would happen to your code if you needed to add a
phone_numberfield to every student? With OOP, how easy is that change?
Completion Checklist
Use this to confirm you have mastered the lesson. Check off each item:
- I can explain what OOP is and why it exists
- I understand the difference between a class and an object
- I can create a class using the
classkeyword - I can create objects from a class using
ClassName() - I know what
__init__()is and when it runs automatically - I can add properties to an object using
self.property = valueinside__init__() - I understand what
selfrefers to and why it is the first parameter - I can define methods inside a class that use
selfto access properties - I can use the dot notation (
.) to access an object’s properties and methods - I know how to use default values in
__init__() - I completed all three guided exercises
- I completed the mini-project
- I understand all six common beginner mistakes
- All source tutorial content preserved
Lesson Summary
In this lesson, you learned the foundations of Object-Oriented Programming in Python.
OOP is a style of programming that organises code around objects — self-contained units that bundle data (properties) and behaviour (methods) together. It makes code cleaner, easier to reuse, and mirrors how the real world works.
A class is a blueprint or template. An object is a specific instance created from that blueprint. You create a class with the class keyword and objects by calling the class like a function.
The __init__() method is a special method that runs automatically every time a new object is created. It is used to set up the object’s initial data by assigning values to properties using self.
The self parameter is a reference to the current object. It is always the first parameter of any method and is how the method knows which object’s data to read or modify. Python fills in self automatically when you call a method — you never pass it manually.
Together, classes, objects, __init__(), and self form the backbone of how professional Python code is written. In the next lessons, you will build on this foundation to learn about class properties, class methods, inheritance, and more.
Sources: W3Schools Python OOP, Classes/Objects, init Method, and self Parameter tutorials.