Object-Oriented Programming (OOP) is a programming approach where we organize code around objects that contain:
Data → attributes/properties
Behavior → methods/functions
Python is a powerful object-oriented language, and OOP becomes very important when you start building larger applications, Data Engineering frameworks, APIs, ML systems, and AI applications.
1.12.1Why Do We Need OOP?
Imagine you want to store information about employees.
Without OOP:
employee1_name = "Sreehari"
employee1_age = 37
employee1_department = "Data Engineering"
employee2_name = "Ravi"
employee2_age = 32
employee2_department = "Analytics"
As the number of employees grows, this becomes difficult to manage.
With OOP:
class Employee:
pass
You can create objects:
employee1 = Employee()
employee2 = Employee()
Each object can contain its own data.
Conceptually:
Employee Class
▼
├── employee1
│ ├── name
│ ├── age
│ └── department
│
└── employee2
├── name
├── age
└── department
1.12.2What Is a Class?
A class is a blueprint or template for creating objects.
Example:
class Employee:
pass
- The class itself doesn't represent a specific employee.
- It defines what an employee object can be.
- Think of:
Class = Blueprint
Object = Actual thing created from blueprint
- For example:
- Class → Employee
- Objects → Sreehari, Ravi, Kiran
1.12.3What Is an Object?
An object is an instance of a class.
Example:
class Employee:
pass
employee1 = Employee()
employee2 = Employee()
- Here:
- Employee → Class
- employee1 → Object
- employee2 → Object
You can verify:
print(type(employee1))
You will get something similar to:
<class '__main__.Employee'>
1.12.4Adding Attributes
We can add data to an object.
class Employee:
pass
employee = Employee()
- employee.name = "Sreehari"
- employee.age = 37
- employee.department = "Data Engineering"
- Now:
print(employee.name)
print(employee.age)
print(employee.department)
Output:
- Sreehari
- 37
- Data Engineering
1.12.5The __init__() Method
Instead of manually assigning attributes after creating an object, Python provides the special method:
__init__()
Example:
class Employee:
def __init__(self, name, age, department):
self.name = name
self.age = age
self.department = department
Now create an object:
employee = Employee(
"Sreehari",
37,
"Data Engineering"
)
The values are automatically assigned.
print(employee.name)
print(employee.age)
print(employee.department)
Output:
- Sreehari
- 37
- Data Engineering
1.12.6Understanding self
This is one of the most important concepts in Python OOP.
Consider:
class Employee:
def __init__(self, name, age):
self.name = name
- self.age = age
- Here:
- self.name
- self.age
refer to attributes belonging to the current object.
When we create:
employee1 = Employee("Sreehari", 37)
Python effectively associates the data with employee1.
When we create:
employee2 = Employee("Ravi", 32)
- the data belongs to employee2.
- Conceptually:
- employee1
- ├── name = Sreehari
- └── age = 37
- employee2
- ├── name = Ravi
- └── age = 32
- self refers to the current instance.
1.12.7Instance Attributes
Attributes created using self are usually called instance attributes.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
Create:
employee = Employee(
"Sreehari",
100000
)
- Here:
- self.name
- self.salary
- are instance attributes.
1.12.8Methods
A function defined inside a class is called a method.
Example:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def display(self):
print(self.name)
print(self.salary)
Call:
employee = Employee("Sreehari", 100000)
employee.display()
Output:
Sreehari
100000
1.12.9Method with Parameters
Methods can accept parameters.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def give_bonus(self, percentage):
bonus = self.salary * percentage / 100
return bonus
Use:
employee = Employee(
"Sreehari",
100000
)
bonus = employee.give_bonus(10)
print(bonus)
Output:
10000.0
1.12.10Class Attributes
A class can also have attributes shared by the class.
class Employee:
company = "ABC Technologies"
def __init__(self, name):
self.name = name
Now:
employee1 = Employee("Sreehari")
employee2 = Employee("Ravi")
print(employee1.company)
print(employee2.company)
- Both can access:
- ABC Technologies
- Conceptually:
Employee Class
▼
└── company = ABC Technologies
↑
┌─────┴─────┐
│ │
employee1 employee2
1.12.11Instance vs Class Attributes
| Feature | Instance Attribute | Class Attribute |
|---|
| Belongs to | Individual object | Class |
| Example | self.name | company |
| Can differ per object? | Yes | Usually shared |
| Defined | Usually in __init__ | Directly in class |
Example:
class Employee:
company = "ABC"
def __init__(self, name):
self.name = name
Here:
company → class attribute
name → instance attribute
1.12.12Four Main Principles of OOP
The four commonly taught pillars are:
OOP
│
┌───────┼────────┐
│ │ │
Encapsulation Inheritance
│ │ │
Abstraction Polymorphism
Let's understand each.
1.12.13Encapsulation
Encapsulation means keeping data and the operations that work on that data together, while controlling how the internal state is accessed or modified.
Example:
class BankAccount:
def __init__(self, balance):
self.balance = balance
def deposit(self, amount):
if amount > 0:
self.balance += amount
Instead of allowing arbitrary modifications through many places in your application, you can provide controlled methods such as:
account.deposit(5000)
1.12.14Public Attributes
Python doesn't enforce traditional private/public access modifiers in the same way as some languages.
A normal attribute is generally considered public:
class Employee:
def __init__(self, name):
self.name = name
You can access:
employee.name
1.12.15Protected Convention — _
A single underscore is commonly used as a convention indicating an internal attribute:
class Employee:
def __init__(self):
self._salary = 50000
The underscore communicates:
This is intended for internal use.
It is not a strict access restriction.
1.12.16Name Mangling — __
Double underscore at the beginning triggers name mangling.
class Employee:
def __init__(self):
self.__salary = 50000
- Direct access:
- employee.__salary
- will normally fail.
Python internally transforms the name approximately to:
_Employee__salary
This mechanism is called name mangling.
It is useful for avoiding accidental name collisions in subclasses, rather than providing absolute security.
1.12.17Properties
Python provides property for controlled attribute access.
Example:
class Employee:
def __init__(self, salary):
self._salary = salary
@property
def salary(self):
return self._salary
Now:
employee = Employee(100000)
print(employee.salary)
You access it like an attribute while the class controls how the value is retrieved.
1.12.18Property Setter
You can also control assignment.
class Employee:
def __init__(self, salary):
self.salary = salary
@property
def salary(self):
return self._salary
@salary.setter
def salary(self, value):
if value < 0:
raise ValueError(
"Salary cannot be negative"
)
self._salary = value
Now:
employee = Employee(100000)
- employee.salary = 120000
- works.
- But:
- employee.salary = -5000
- raises:
- ValueError
This is a practical example of encapsulation.
1.12.19Inheritance
Inheritance allows one class to reuse or extend another class.
Example:
class Employee:
def work(self):
print("Employee is working")
class DataEngineer(Employee):
def build_pipeline(self):
print("Building data pipeline")
Now:
engineer = DataEngineer()
engineer.work()
engineer.build_pipeline()
Output:
- Employee is working
- Building data pipeline
- DataEngineer inherits from Employee.
1.12.20Parent and Child Classes
Terminology:
Employee
▼
Parent / Base / Superclass
DataEngineer
▼
Child / Derived / Subclass
The child can use functionality defined by the parent.
1.12.21Overriding Methods
A child class can provide its own implementation of a parent method.
class Employee:
def work(self):
print("Employee working")
class DataEngineer(Employee):
def work(self):
print("Data Engineer building pipelines")
Now:
employee = Employee()
engineer = DataEngineer()
employee.work()
engineer.work()
Output:
Employee working
Data Engineer building pipelines
This is method overriding.
1.12.22super()
super() allows a child class to call functionality from its parent class.
class Employee:
def __init__(self, name):
self.name = name
class DataEngineer(Employee):
def __init__(self, name, technology):
super().__init__(name)
self.technology = technology
Create:
engineer = DataEngineer(
"Sreehari",
"Azure"
)
Now:
print(engineer.name)
print(engineer.technology)
Output:
Sreehari
Azure
1.12.23Why Use super()?
- Suppose the parent class already performs initialization.
- Instead of duplicating:
- self.name = name
- the child can reuse:
- super().__init__(name)
- This reduces duplicated code.
1.12.24Multiple Inheritance
Python allows a class to inherit from multiple classes.
class Employee:
def work(self):
print("Working")
class AzureSkill:
def deploy(self):
print("Deploying to Azure")
class DataEngineer(Employee, AzureSkill):
pass
Now:
engineer = DataEngineer()
engineer.work()
engineer.deploy()
Output:
Working
Deploying to Azure
Python supports multiple inheritance, although it should be used thoughtfully.
1.12.25Method Resolution Order — MRO
When multiple inheritance is involved, Python needs to determine which method to use.
This is called Method Resolution Order (MRO).
You can inspect it:
print(DataEngineer.mro())
For example, Python may show a sequence similar to:
- DataEngineer
- Employee
- AzureSkill
- object
The exact order depends on the inheritance structure.
1.12.26Polymorphism
Polymorphism means that different objects can respond to the same operation in their own way.
Example:
class CSVProcessor:
def process(self):
print("Processing CSV")
class JSONProcessor:
def process(self):
print("Processing JSON")
Now:
processors = [
CSVProcessor(),
JSONProcessor()
]
for processor in processors:
processor.process()
Output:
- Processing CSV
- Processing JSON
- The same method call:
- processor.process()
- behaves differently depending on the object.
1.12.27Duck Typing
Python often relies on duck typing.
- The idea is:
- If an object supports the required operation, we can use it.
- For example:
def process_data(processor):
processor.process()
- Any object with a compatible process() method can potentially be passed in.
- process_data(CSVProcessor())
- process_data(JSONProcessor())
Python doesn't require both classes to inherit from the same base class just for this simple interface.
This is an important Python concept.
1.12.28Abstraction
- Abstraction means exposing the important interface while hiding implementation details.
- For example, when you call:
- model.predict(data)
you don't need to know every internal mathematical operation performed by the model.
Python supports formal abstraction using the abc module.
1.12.29Abstract Base Class
from abc import ABC, abstractmethod
class DataProcessor(ABC):
@abstractmethod
def process(self, data):
pass
A subclass must implement process():
class CSVProcessor(DataProcessor):
def process(self, data):
print("Processing CSV")
And:
class JSONProcessor(DataProcessor):
def process(self, data):
print("Processing JSON")
This establishes a common interface.
1.12.30Creating an Abstract Class
from abc import ABC, abstractmethod
class Pipeline(ABC):
@abstractmethod
def run(self):
pass
Now:
class SalesPipeline(Pipeline):
def run(self):
print("Running sales pipeline")
You can create:
pipeline = SalesPipeline()
pipeline.run()
But you generally cannot instantiate the abstract class itself:
Pipeline()
because it contains an abstract method.
1.12.31Composition
Another important OOP concept is composition.
Instead of inheriting, one object can contain another object.
Example:
class Database:
def connect(self):
print("Database connected")
class DataPipeline:
def __init__(self):
self.database = Database()
def run(self):
self.database.connect()
print("Pipeline running")
Now:
pipeline = DataPipeline()
pipeline.run()
Conceptually:
DataPipeline
▼
└── Database
This represents a has-a relationship.
Inheritance often represents an is-a relationship.
1.12.32Inheritance vs Composition
- Inheritance
- DataEngineer IS AN Employee
- Composition
- DataPipeline HAS A Database
- A useful rule is:
IS-A → consider inheritance
HAS-A → consider composition
In production software, composition is often preferred when inheritance isn't a natural relationship.
1.12.33Static Methods
- Sometimes a method doesn't need access to the object or class.
- Use:
- @staticmethod
Example:
class MathUtils:
@staticmethod
def add(a, b):
return a + b
Call:
result = MathUtils.add(10, 20)
print(result)
Output:
30
No object is required.
1.12.34Class Methods
- A class method receives the class itself as the first argument, conventionally called cls.
- Use:
- @classmethod
Example:
class Employee:
company = "ABC"
@classmethod
def get_company(cls):
return cls.company
Call:
print(Employee.get_company())
Output:
ABC
1.12.35Instance vs Class vs Static Methods
| Method | First argument | Access |
|---|
| Instance method | self | Object data |
| Class method | cls | Class data |
| Static method | None automatically | Independent utility |
Example:
class Example:
def instance_method(self):
pass
@classmethod
def class_method(cls):
pass
@staticmethod
def static_method():
pass
1.12.36Magic / Dunder Methods
Python has special methods surrounded by double underscores.
Examples:
__init__
__str__
__repr__
__len__
__eq__
These are often called dunder methods.
1.12.37__str__()
__str__() controls the user-friendly string representation of an object.
Without it:
class Employee:
def __init__(self, name):
self.name = name
Printing the object gives a generic representation.
With:
class Employee:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Employee: {self.name}"
Now:
employee = Employee("Sreehari")
print(employee)
Output:
Employee: Sreehari
1.12.38__repr__()
__repr__() is intended to provide a useful representation of an object, particularly for developers and debugging.
Example:
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return (
f"Employee(name={self.name!r}, "
f"age={self.age!r})"
)
Then:
employee = Employee("Sreehari", 37)
print(employee)
Depending on how __str__ is defined, Python may use the appropriate representation.
1.12.39__len__()
You can define how len() behaves for your object.
class DataSet:
def __init__(self, records):
self.records = records
def __len__(self):
return len(self.records)
Now:
data = DataSet([10, 20, 30, 40])
print(len(data))
Output:
4
1.12.40__eq__()
You can define how equality works.
class Employee:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
Then:
e1 = Employee("Sreehari")
e2 = Employee("Sreehari")
print(e1 == e2)
Output:
True
1.12.41Dataclasses
Python provides dataclasses for classes that mainly store data.
Example:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
- age: int
- department: str
- Now:
employee = Employee(
"Sreehari",
37,
"Data Engineering"
)
You automatically get useful functionality such as an initializer and a readable representation.
print(employee)
Output will be similar to:
Employee(name='Sreehari', age=37, department='Data Engineering')
Dataclasses are very useful for clean data models.
1.12.42OOP Example — Data Engineering
Let's design a simple pipeline object.
class DataPipeline:
def __init__(self, name):
self.name = name
self.status = "NOT_STARTED"
def start(self):
self.status = "RUNNING"
print(f"{self.name} started")
def complete(self):
self.status = "SUCCESS"
print(f"{self.name} completed")
def fail(self):
self.status = "FAILED"
print(f"{self.name} failed")
Use:
pipeline = DataPipeline("Customer_Load")
pipeline.start()
pipeline.complete()
print(pipeline.status)
Output:
- Customer_Load started
- Customer_Load completed
- SUCCESS
1.12.43OOP ETL Pipeline
We can go further.
class ETLPipeline:
def __init__(self, name):
self.name = name
def extract(self):
print("Extracting data")
def transform(self):
print("Transforming data")
def load(self):
print("Loading data")
def run(self):
self.extract()
- self.transform()
- self.load()
- Now:
pipeline = ETLPipeline("Sales Pipeline")
pipeline.run()
Output:
- Extracting data
- Transforming data
- Loading data
This is much cleaner than putting everything into one large procedural script.
1.12.44Adding Exception Handling to OOP
We can combine OOP with the previous lesson.
class ETLPipeline:
def __init__(self, name):
self.name = name
def extract(self):
print("Extracting")
def transform(self):
print("Transforming")
def load(self):
print("Loading")
def run(self):
try:
self.extract()
self.transform()
self.load()
except Exception as error:
print(
f"{self.name} failed: {error}"
)
else:
print(
f"{self.name} completed successfully"
)
Use:
pipeline = ETLPipeline("Customer Pipeline")
- pipeline.run()
- This combines:
- OOP
+
Functions
+
Exception Handling
1.12.45Data Source Abstraction
Imagine your system supports multiple sources.
class DataSource:
def read(self):
raise NotImplementedError
CSV:
class CSVSource(DataSource):
def read(self):
print("Reading CSV")
Database:
class DatabaseSource(DataSource):
def read(self):
print("Reading Database")
API:
class APISource(DataSource):
def read(self):
print("Reading API")
Now:
sources = [
CSVSource(),
DatabaseSource(),
APISource()
]
for source in sources:
source.read()
Output:
- Reading CSV
- Reading Database
- Reading API
- This demonstrates polymorphism.
1.12.46OOP in Machine Learning
OOP is also common in ML.
Imagine a generic model:
class Model:
def train(self, data):
print("Training model")
def predict(self, data):
print("Generating predictions")
A specialized model:
class ChurnModel(Model):
def train(self, data):
print("Training churn model")
Now:
model = ChurnModel()
model.train(data)
model.predict(data)
This pattern resembles how many ML frameworks organize models and components.
1.12.47OOP in APIs
Suppose you're building an API client.
class APIClient:
def __init__(self, base_url):
self.base_url = base_url
def get(self, endpoint):
print(
f"GET {self.base_url}{endpoint}"
)
Use:
client = APIClient(
"https://api.example.com"
)
client.get("/customers")
This lets you encapsulate API configuration and behavior inside an object.
1.12.48OOP in AI Applications
A simple AI assistant could be modeled as:
class AIAssistant:
def __init__(self, model):
self.model = model
def ask(self, question):
return self.model.generate(question)
Then:
assistant = AIAssistant(model)
response = assistant.ask(
"What is machine learning?"
)
This type of object-oriented structure becomes useful when building larger AI systems.
1.12.49Composition in an AI Pipeline
A realistic architecture might look like:
AIApplication
▼
├── DocumentLoader
▼
├── TextProcessor
▼
├── EmbeddingModel
▼
├── VectorStore
│
└── LLM
Each component has its own responsibility.
For example:
class RAGPipeline:
def __init__(
self,
loader,
embedder,
vector_store,
llm
):
self.loader = loader
- self.embedder = embedder
- self.vector_store = vector_store
- self.llm = llm
This is composition and becomes extremely useful when you study Generative AI, RAG, LangChain, LangGraph, and AI Agents later in your course.
1.12.50SOLID Principles
- As you become more advanced, you'll encounter the SOLID principles.
- They help design maintainable object-oriented software.
- S → Single Responsibility Principle
- O → Open/Closed Principle
- L → Liskov Substitution Principle
- I → Interface Segregation Principle
- D → Dependency Inversion Principle
- You don't need to master all of these immediately.
For now, remember the most important one:
- Single Responsibility Principle
- A class should have a clear responsibility rather than doing everything.
- Bad:
class DataSystem:
def read_database(self):
...
def send_email(self):
...
def train_model(self):
...
def generate_report(self):
...
def upload_to_azure(self):
...
- Better:
- DatabaseReader
- EmailService
- ModelTrainer
- ReportGenerator
- AzureUploader
- Each class has a clear responsibility.
1.12.51When Should You Use OOP?
- OOP becomes particularly useful when:
- Your application is large
- You have many related objects
- State needs to be maintained
- You need reusable components
- You need clear abstractions
- Multiple developers work on the project
- You are building frameworks or services
- For a tiny script:
print("Hello")
- creating a class would usually be unnecessary.
- Don't use OOP simply because you can.
- Use it when it makes the design clearer.
1.12.52OOP vs Procedural Programming
Procedural
def extract():
...
def transform():
...
def load():
...
- extract()
- transform()
- load()
- Object-Oriented
class Pipeline:
def extract(self):
...
def transform(self):
...
def load(self):
...
def run(self):
self.extract()
- self.transform()
- self.load()
- Neither approach is universally better.
- Python supports both styles.
For larger applications, OOP can provide useful structure.
1.12.53Important OOP Terminology
| Term | Meaning |
|---|
| Class | Blueprint for objects |
| Object | Instance of a class |
| Attribute | Data stored on an object/class |
| Method | Function defined inside a class |
| self | Current instance |
| __init__ | Initializer method |
| Encapsulation | Organizing/protecting state and behavior |
| Inheritance | Reusing/extending another class |
| Polymorphism | Same interface, different behavior |
| Abstraction | Exposing essential interface |
| Composition | Building objects from other objects |
| super() | Access parent-class behavior |
| @staticmethod | Method without automatic self/cls |
| @classmethod | Method receiving the class as cls |
| Property | Controlled attribute-style access |
| Dataclass | Convenient data-oriented class |
1.12.54Practice Exercise 1 — Employee
Create a class:
class Employee:
...
- It should contain:
- name
- age
- department
- salary
- Create two employee objects.
1.12.55Practice Exercise 2 — Salary
- Add:
- calculate_bonus()
- The method should accept a percentage.
Example:
employee.calculate_bonus(10)
1.12.56Practice Exercise 3 — Bank Account
Create:
class BankAccount:
with:
account_number
- holder_name
- balance
- Methods:
- deposit()
- withdraw()
- check_balance()
Do not allow withdrawal if there isn't enough balance.
1.12.57Practice Exercise 4 — Data Pipeline
Create:
class DataPipeline:
with methods:
extract()
- transform()
- load()
- run()
- run() should call:
extract()
▼
transform()
▼
load()
1.12.58Practice Exercise 5 — Inheritance
Create:
- Employee should have:
- work()
- DataEngineer should have:
- build_pipeline()
1.12.59Practice Exercise 6 — Polymorphism
- Create:
- CSVProcessor
- JSONProcessor
- ExcelProcessor
- Each should have:
- process()
- Then:
processors = [
CSVProcessor(),
JSONProcessor(),
ExcelProcessor()
]
for processor in processors:
processor.process()
1.12.60Practice Exercise 7 — Custom Exception + OOP
Create:
class DataQualityError(Exception):
pass
Then create:
class DataValidator:
...
with:
validate_record_count()
Raise DataQualityError when source and target counts don't match.
1.12.61Interview Questions
1. What is OOP?
OOP is a programming paradigm that organizes software around objects containing data and behavior.
2. What is a class?
A class is a blueprint for creating objects.
3. What is an object?
An object is an instance of a class.
4. What is self?
self refers to the current object instance in an instance method.
5. What is __init__()?
It is an initializer method that runs when an object is created.
6. What is inheritance?
Inheritance allows one class to reuse or extend another class.
7. What is polymorphism?
It allows different objects to provide different implementations of a common operation/interface.
8. What is encapsulation?
It means combining data and related behavior and controlling access to internal state.
9. What is abstraction?
Abstraction exposes essential functionality while hiding implementation details.
10. What is super()?
super() provides access to methods and initialization logic from a parent class.
11. What is method overriding?
When a child class provides its own implementation of a method inherited from a parent class.
12. What is composition?
Composition is building a class using instances of other classes.
13. What is the difference between @staticmethod and @classmethod?
A static method doesn't receive an automatic self or cls; a class method receives the class as cls.
14. What is multiple inheritance?
A class inheriting from more than one parent class.
15. What is duck typing?
Python's approach of focusing on whether an object supports the required behavior rather than requiring a specific declared type.
1.12.62The Most Important OOP Example
Put everything together:
class DataPipeline:
pipeline_count = 0
def __init__(self, name):
self.name = name
self.status = "NOT_STARTED"
DataPipeline.pipeline_count += 1
def start(self):
self.status = "RUNNING"
print(f"{self.name} started")
def complete(self):
self.status = "SUCCESS"
print(f"{self.name} completed")
def fail(self):
self.status = "FAILED"
print(f"{self.name} failed")
def __str__(self):
return (
f"Pipeline("
f"name={self.name}, "
f"status={self.status})"
)
Create objects:
pipeline1 = DataPipeline("Customer Load")
pipeline2 = DataPipeline("Orders Load")
- Run them:
- pipeline1.start()
- pipeline1.complete()
- pipeline2.start()
- pipeline2.fail()
- Print:
print(pipeline1)
print(pipeline2)
Output:
- Customer Load started
- Customer Load completed
- Orders Load started
- Orders Load failed
- Pipeline(name=Customer Load, status=SUCCESS)
- Pipeline(name=Orders Load, status=FAILED)
- And:
print(DataPipeline.pipeline_count)
- gives:
- 2
- This one example demonstrates:
↓
__init__
↓
self
▼
Instance attributes
▼
Class attributes
▼
Methods
↓
__str__
1.12.63OOP Mental Model
Keep this model in your mind:
CLASS
│
┌────────┴────────┐
│ │
DATA BEHAVIOR
│ │
Attributes Methods
│ │
└────────┬────────┘
↓
OBJECT
│
┌────────┼────────┐
↓ ↓ ↓
Object 1 Object 2 Object 3
And the four major OOP concepts:
OOP
│
┌───────────┼───────────┐
↓ ↓ ↓
Encapsulation Inheritance Polymorphism
│
↓
Abstraction
For your Data Engineering path, focus especially on:
- Classes and objects
- __init__() and self
- Instance vs class attributes
- Methods
- Inheritance
- Method overriding
- super()
- Polymorphism
- Composition
- Exception handling inside classes
- Properties
- Dataclasses
These concepts will become very useful later when you work with APIs, ML pipelines, FastAPI, Flask, MLflow, LangChain, LangGraph, RAG systems, and AI agents.
Next lesson: 1.13 Iterators & Generators — iter(), next(), custom iterators, yield, generator functions, generator expressions, memory efficiency, and how generators are useful for processing large datasets.