Lesson 16: Classes, Instances & Inheritance
Object structure, __init__ constructor, instance parameters, methods, and parent-child overrides.
The OOP Paradigm
Object-Oriented Programming (OOP) is a design philosophy that groups related data (attributes) and behavior (methods) into cohesive packages called objects. Instead of scattering data and functions around your program, you model real-world things — a User, an Order, a Course — as objects where the data and its operations live together.
Before OOP, programs were a pile of variables and functions: user_name, user_age, print_user(user). OOP bundles them: a User object holds its own data and knows how to act. This bundling — called encapsulation — keeps programs organized as they grow from 100 lines to 100,000 lines.
What You'll Learn in This Lesson
- Define classes and create instances
- Initialize objects with
__init__ - Write methods and understand
self - Inherit from parent classes and override methods
Class vs Instance
| Term | Meaning | Analogy |
|---|---|---|
| Class | The blueprint / template | A cookie cutter |
| Instance | A concrete object made from the class | One cookie stamped out |
class Person:
def __init__(self, name, role):
self.name = name
self.role = role
def get_profile(self):
return f"Name: {self.name}, Role: {self.role}"
user = Person("Rahul", "Student") # user is an INSTANCE
print(user.get_profile()) # Name: Rahul, Role: Student
Mental model: the class is the blueprint — it never exists as a physical thing. The instance is what you actually use. You can stamp out unlimited instances from one blueprint, each with its own data.
init — The Constructor
__init__ is the initializer: it runs automatically the moment you create an instance. It receives the creation arguments and stores them on the object:
class Person:
def __init__(self, name, role): # runs on Person(...)
self.name = name # store ON the object
self.role = role
The self parameter is the object itself — it is passed automatically, so you never pass it explicitly.
Mental model:
self.name = namemeans "attach a label called name to THIS object, pointing at the value". Each instance gets its own independent labels.
What if you skip __init__? Instances still work, but every attribute must be set manually after creation:
class Empty:
pass
e = Empty()
e.name = "Amol" # works, but clunky — attributes appear ad-hoc
The __init__ method guarantees every instance is born fully formed with the right attributes — the hallmark of reliable OOP.
Methods — Functions Attached to Objects
A method is a function defined inside a class. Its first parameter is always self:
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
return "Woof!"
def describe(self):
return f"{self.name} is a {self.breed}"
Call a method on an instance with the dot: dog.bark(). Python automatically passes dog as self.
Why self is passed automatically: dog.bark() is syntactic sugar for Dog.bark(dog). Python injects the receiver as the first argument — that's why every method declares self first, even when it takes no other arguments.
Class attributes vs instance attributes:
| Kind | Defined | Shared? | Example |
|---|---|---|---|
| Class attribute | Inside class body | Shared by all instances | species = "Canis" |
| Instance attribute | In __init__ via self |
Unique per instance | self.name |
class Dog:
species = "Canis familiaris" # class attribute — shared
def __init__(self, name):
self.name = name # instance attribute — unique
d1, d2 = Dog("Rocky"), Dog("Bella")
print(d1.species, d2.species) # same for both
print(d1.name, d2.name) # different for each
Inheritance — Child Classes Reuse Parent Code
A child class inherits all attributes and methods from its parent, then adds or overrides what it needs. This expresses "is-a" relationships: an Instructor is a Person.
class Instructor(Person): # (Person) = inherit
def __init__(self, name, department, course):
super().__init__(name, role="Instructor") # call parent's __init__
self.department = department
self.course = course
def get_profile(self): # OVERRIDE the parent method
parent = super().get_profile()
return f"{parent} | Dept: {self.department} | Course: {self.course}"
Key points:
super()gives access to the parent class —super().__init__(...)reuses the parent's setup without repeating code.- Overriding means redefining a parent method in the child; the child's version wins for child instances.
- A child gets everything the parent has — attributes, methods, even the parent's parent.
Mental model: inheritance is a family tree. The child is born with all the parent's traits and can add its own or change inherited ones.
super()is the "call mom" button — reuse the parent's implementation instead of rewriting it.
Why OOP?
| Benefit | Explanation |
|---|---|
| Cohesion | Data + behavior live together |
| Reuse | Inherit instead of copy-paste |
| Modeling | Code mirrors the real world |
| Maintainability | Change one class, not every call site |
When should you NOT use OOP? For tiny scripts, plain functions are simpler and perfectly fine. OOP earns its keep when you have many objects sharing structure and behavior — user accounts, products, database models, UI components. The rule: functions first; classes when you see duplication across related data.
Common Mistakes to Avoid
- Mistake: Forgetting
selfas the first parameter — Fix: every instance method needsselffirst. - Mistake: Defining a class but never creating an instance — Fix:
obj = MyClass(...)actually runs the code. - Mistake: Repeating parent setup instead of
super().__init__(...)— Fix: call super and add only what's new. - Mistake: Naming classes in snake_case — Fix: classes use PascalCase (
BankAccount), functions/variables use snake_case. - Mistake: Putting default attributes at the class level when they should be per-instance — Fix: mutable defaults belong in
__init__viaself.
Professional Tips & Tricks
super().__init__(...)keeps the parent setup without repeating code.- Name classes in PascalCase (BankAccount) and methods/attributes in snake_case.
- Give every class a docstring describing its responsibility.
- Initialize all attributes in
__init__— objects should be born complete. - Use class attributes for shared constants; instance attributes for per-object data.
Key Takeaways
- A class is a blueprint; an instance is a concrete object.
__init__initializes every new instance.selfis the object itself, passed automatically.- Inheritance reuses parent code;
super()reaches the parent. - Overriding lets a child redefine a parent method.
- Class attributes are shared; instance attributes are per-object.
Next up: Inheritance & polymorphism in depth.
# Class constructor and Inheritance demo
class Person:
def __init__(self, name, role):
self.name = name
self.role = role
def get_profile(self):
return f"Name: {self.name}, Role: {self.role}"
# Child inherits from Person parent class
class Instructor(Person):
def __init__(self, name, department, course):
# Initialize parent attributes
super().__init__(name, role="Instructor")
self.department = department
self.course = course
# Override get_profile method
def get_profile(self):
parent_details = super().get_profile()
return f"{parent_details} | Dept: {self.department} | Course: {self.course}"
# Instantiate objects
user = Person("Rahul", "Student")
teacher = Instructor("Amol Shukla", "AI Engineering", "Data Science")
print(user.get_profile())
print(teacher.get_profile())Lesson Code (Python)
# Class constructor and Inheritance demo
class Person:
def __init__(self, name, role):
self.name = name
self.role = role
def get_profile(self):
return f"Name: {self.name}, Role: {self.role}"
# Child inherits from Person parent class
class Instructor(Person):
def __init__(self, name, department, course):
# Initialize parent attributes
super().__init__(name, role="Instructor")
self.department = department
self.course = course
# Override get_profile method
def get_profile(self):
parent_details = super().get_profile()
return f"{parent_details} | Dept: {self.department} | Course: {self.course}"
# Instantiate objects
user = Person("Rahul", "Student")
teacher = Instructor("Amol Shukla", "AI Engineering", "Data Science")
print(user.get_profile())
print(teacher.get_profile())Console Output
Name: Rahul, Role: Student
Name: Amol Shukla, Role: Instructor | Dept: AI Engineering | Course: Data ScienceCode Visualization Tips
- Picture the class as a cookie cutter and instances as the cookies it stamps out.
- Draw one box per instance with its own attribute values — each box is independent.
- Visualize inheritance as a family tree: the child inherits the parent's traits and adds its own.
Professional Tips & Tricks
- super().__init__(...) keeps the parent setup without repeating code.
- Name classes in PascalCase (BankAccount) and methods/attributes in snake_case.
- Give every class a docstring describing its responsibility.
Python Code Judge & Practice Arena
LeetCode StyleRun real Python 3.12 WebAssembly code directly in your browser against automated test suites.
Problem 1: BankAccount Class 1
Test Your Knowledge
Instant feedbackQuick Check: Lesson 16: Classes, Objects, `__init__`, `self` & Instance State
Up next · Continue learning
Inheritance & Polymorphism
Deep-dive into inheritance, method overriding, polymorphism, isinstance, and the MRO.