1. Introduction to Object-Oriented Programming (OOP)
Welcome to Module 4 of our Python Mastery Series: Object-Oriented Programming (OOP) in Depth! Up to this point, our code has been primarily procedural and functional—organizing logic into sequences of instructions, loops, conditionals, and independent utility functions that manipulate built-in primitives or collection data structures.
As applications scale to enterprise levels (like managing millions of customer user sessions, modeling complex banking accounts, or engineering game engines), procedural code can become fragmented. Data state and the functions that manipulate that data remain separated, leading to scope pollution and maintenance challenges.
Object-Oriented Programming is a software paradigm that unifies data attributes and behavior methods into cohesive, self-contained entities known as Objects, constructed according to formal blueprints called Classes.
2. Fundamental Concepts: Classes vs Objects
1. What is a Class?
A Class is a user-defined prototype or architectural blueprint. It defines the structure (attributes/variables) and actions (methods/functions) that all objects created from it will possess. A class creates a new custom data type in Python's memory space.
2. What is an Object (Instance)?
An Object (or instance) is a concrete realization of a class allocated in Python Heap memory. If Car is a class blueprint, then your red Tesla parked in the driveway is an object instance instantiated from that class.
- Memory Status
- Logical structure definition stored once in code memory.
- Physical entity allocated in Heap memory with its own memory address (`id()`).
- Analogy
- Architectural blueprint for a house.
- The physical house built on a specific street plot.
- Python Syntax
- Declared using
class ClassName: - Instantiated calling
obj = ClassName()
| OOP Conceptual Dimension | Class (Blueprint) | Object (Instance) |
|---|---|---|
3. Mechanics of Class Definition and the `self` Parameter
1. Class Header Declaration
In Python, classes are declared using the class keyword followed by the class identifier in PascalCase (e.g., BankAccount, UserProfile), following PEP 8 naming standards.
2. Demystifying the `self` Parameter
Every instance method inside a Python class MUST accept an explicit first parameter, conventionally named self.
self variable is an explicit reference to the current specific object instance invoking the method. When you call account.deposit(100), Python automatically translates the statement behind the scenes to: BankAccount.deposit(account, 100)! The active instance reference is passed into self automatically.
class BasicUser:
"""A minimal class representing a user entity."""
pass
# Instantiating objects (calling the class constructor)
user_1 = BasicUser()
user_2 = BasicUser()
print("=== Class Type and Memory Instance Inspection ===")
print("Type of BasicUser Class :", type(BasicUser))
print("Instance user_1 Type :", type(user_1))
print("Instance user_1 Memory ID:", id(user_1))
print("Instance user_2 Memory ID:", id(user_2))
print("Are Instance IDs Unique? :", id(user_1) != id(user_2))
=== Class Type and Memory Instance Inspection === Type of BasicUser Class :Instance user_1 Type : Instance user_1 Memory ID: 140239482910224 Instance user_2 Memory ID: 140239482911856 Are Instance IDs Unique? : True
4. The Constructor Method (`__init__`) and Instance Attributes
When an object is instantiated from a class, Python automatically invokes a special dunder method: __init__(), formally known as the Constructor Initializer.
1. Attribute Binding in `__init__`
The __init__ method initializes the starting state of an object by binding incoming arguments to instance variables attached to self (e.g., self.attribute_name = argument_value).
class BankAccount:
"""Models a financial bank account entity with balance state and methods."""
def __init__(self, account_number: str, owner_name: str, initial_balance: float = 0.0):
# Binding parameters to instance attributes attached to self
self.account_number = account_number
self.owner_name = owner_name
self.balance = initial_balance
print(f"[CONSTRUCTOR] Account #{self.account_number} created for '{self.owner_name}'.")
def display_statement(self):
"""Instance method accessing self instance attributes."""
print(f"Account: {self.account_number} | Owner: {self.owner_name:<12} | Balance: ${self.balance:,.2f}")
# Instantiating objects with custom initialization state
acc_a = BankAccount("ACC_1001", "Alice Smith", 5000.0)
acc_b = BankAccount("ACC_1002", "Bob Jones", 1200.0)
print("\n--- Displaying Account Statements ---")
acc_a.display_statement()
acc_b.display_statement()
[CONSTRUCTOR] Account #ACC_1001 created for 'Alice Smith'. [CONSTRUCTOR] Account #ACC_1002 created for 'Bob Jones'. --- Displaying Account Statements --- Account: ACC_1001 | Owner: Alice Smith | Balance: $5,000.00 Account: ACC_1002 | Owner: Bob Jones | Balance: $1,200.00
5. Instance Attributes vs Class Attributes
In Object-Oriented Python, attributes exist at two distinct architectural levels:
1. Instance Attributes (`self.attr`)
Variables defined inside methods (usually __init__) using self. Every object instance maintains its own completely independent copy of instance attributes in its personal __dict__ memory space.
2. Class Attributes
Variables declared directly inside the class .tech-lesson-content, outside any methods. Class attributes are shared globally across ALL object instances instantiated from that class!
instance.class_attr = 99) does NOT update the shared class variable! Instead, it creates a new **Instance Attribute** on that specific object that "shadows" (masks) the class variable! To mutate a class attribute globally, access it via the Class name directly: ClassName.class_attr = 99.
class Employee:
# Class Attribute (Shared globally across all instances)
COMPANY_NAME = "Apex Global Tech"
total_employee_count = 0
def __init__(self, emp_id: int, name: str):
# Instance Attributes (Unique per object)
self.emp_id = emp_id
self.name = name
# Increment shared class counter via Class Name
Employee.total_employee_count += 1
# Instantiating multiple employees
e1 = Employee(101, "Alice")
e2 = Employee(102, "Bob")
print(f"Employee 1 Company: {e1.COMPANY_NAME} | Name: {e1.name}")
print(f"Employee 2 Company: {e2.COMPANY_NAME} | Name: {e2.name}")
print(f"Total Active Staff: {Employee.total_employee_count}")
# Demonstrating Global Mutation via Class Name
Employee.COMPANY_NAME = "Apex International Solutions"
print("\n--- After Mutating Employee.COMPANY_NAME globally ---")
print(f"Employee 1 Updated Company: {e1.COMPANY_NAME}")
print(f"Employee 2 Updated Company: {e2.COMPANY_NAME}")
Employee 1 Company: Apex Global Tech | Name: Alice Employee 2 Company: Apex Global Tech | Name: Bob Total Active Staff: 2 --- After Mutating Employee.COMPANY_NAME globally --- Employee 1 Updated Company: Apex International Solutions Employee 2 Updated Company: Apex International Solutions
6. Integrating Class Instances with `match-case` Structural Pattern Matching
Python 3.10+ Class Pattern Matching allows inspecting custom class instances inside match-case statements directly, matching on instance types, extracting attributes, and applying conditional guards.
class Order:
def __init__(self, order_id: str, total_amount: float, customer_tier: str):
self.order_id = order_id
self.total_amount = total_amount
self.customer_tier = customer_tier
def process_fulfillment_order(order_obj: object):
"""
Routes custom class instance objects using Class Pattern Matching.
Demonstrates inspecting instance attributes dynamically.
"""
match order_obj:
case Order(customer_tier="GOLD", total_amount=amt) if amt >= 1000.0:
discount = amt * 0.20
return f"VIP FULFILLMENT: 20% Tier Discount Applied (-${discount:.2f}). Net: ${amt - discount:.2f}"
case Order(customer_tier="GOLD" | "SILVER", total_amount=amt):
discount = amt * 0.10
return f"STANDARD MEMBER: 10% Discount Applied (-${discount:.2f}). Net: ${amt - discount:.2f}"
case Order(order_id=oid, total_amount=amt):
return f"REGULAR PROCESSING: Full Amount Charged (${amt:.2f}) for Order #{oid}"
case _:
return "ERROR: Provided object is not a valid Order instance."
# Testing Class Instance Pattern Dispatcher
order_1 = Order("ORD_901", 1500.0, "GOLD")
order_2 = Order("ORD_902", 300.0, "SILVER")
order_3 = Order("ORD_903", 100.0, "REGULAR")
print(process_fulfillment_order(order_1))
print(process_fulfillment_order(order_2))
print(process_fulfillment_order(order_3))
VIP FULFILLMENT: 20% Tier Discount Applied (-$300.00). Net: $1200.00 STANDARD MEMBER: 10% Discount Applied (-$30.00). Net: $270.00 REGULAR PROCESSING: Full Amount Charged ($100.00) for Order #ORD_903
7. Frequently Asked Interview Questions with Answers
self is an explicit reference pointing to the active instance invoking the method. It allows methods to access, modify, and bind instance-specific attributes (e.g., self.name) inside instance memory space.
__init__() is an **initializer method**, NOT the raw object allocator. In Python, object allocation is handled first by __new__(), which creates the raw memory object and passes it to __init__() as self to bind instance attributes.
self and are unique to each instance object. Class attributes are declared directly inside the class body outside methods and are shared globally across all instances of that class.
case Order(...)) and inspects its internal instance attributes directly, matching specific values or binding them to local variables.
8. Homework & Practical Assignments
Task 1: Inventory Item Class with Instance Methods
Create a script named inventory_class_task.py inside your lesson_32 folder:
- Define a class
ProductItemwith a constructor__init__(self, product_id, title, price, quantity). - Add an instance method
calculate_total_value(self) -> floatreturningprice * quantity. - Add an instance method
restock(self, amount: int)that increments quantity. - Instantiate 2 product objects, perform restock operations, and print formatted statements using f-strings.
Task 2: Class Pattern Matching User Tier Evaluator
Create a script named class_pattern_task.py:
- Define class
UserProfile(username, credit_score, account_age_months). - Write function
evaluate_credit_tier(user_obj)usingmatch-caseClass Pattern Matching:case UserProfile(credit_score=score) if score >= 750→ Return "PRIME_TIER_APPROVED".case UserProfile(credit_score=score, account_age_months=months) if score >= 650 and months >= 12→ Return "STANDARD_TIER_APPROVED".case UserProfile()→ Return "CREDIT_TIER_REJECTED".
- Test function with 3 user objects and print results.
Task 3: Master Review Capstone Project — Object-Oriented Enterprise ERP System (`oop_enterprise_os.py`)
Create a script named oop_enterprise_os.py inside your lesson_32 folder. This assignment tests and integrates **ALL concepts learned across Lessons 1 through 32**.
Project Architectural Requirements Specification:
- Object-Oriented Architecture (Lesson 32):
- Define core domain classes:
Customer(customer_id, name, email, tier),InventoryItem(item_id, title, price, stock), andTransaction(tx_id, customer_obj, item_obj, qty). - Use
__init__constructors to bind instance attributes and track global counters via class attributes (e.g.,Transaction.total_tx_count).
- Define core domain classes:
- Observability & Diagnostics (Lesson 31):
- Configure a
loggingFileHandler writing diagnostic logs toerp_system.log. - Add internal developer assertions (
assert) inside class methods to check state invariants (e.g., price > 0).
- Configure a
- Context Managers & Fault Tolerance (Lessons 29-30):
- Build a custom context manager
DatabaseSession(db_path)that manages JSON persistence files safely. - Define custom exceptions:
ERPError(Exception),OutOfStockError(ERPError). Wrap transactions in 4-blocktry-except-else-finallysuites.
- Build a custom context manager
- CSV & JSON Persistence Layer (Lessons 27-28):
- Maintain a storage directory
erp_vault/usingpathlib.Path. - Persist object dictionaries to
inventory.jsonand export sales reports tosales_audit.csv.
- Maintain a storage directory
- Nested Collections, Comprehensions & Data Structures (Lessons 21-26):
- Maintain in-memory registry dictionaries mapping IDs to Class Instances.
- Use List and Dictionary Comprehensions to transform instance collections dynamically.
- Advanced Function Parameters & Scope (Lessons 15-17):
- Structure logic into modular functions with type hints, docstrings, early return Guard Clauses, and
*args/**kwargslogging.
- Structure logic into modular functions with type hints, docstrings, early return Guard Clauses, and
- Indefinite & Definite Loops (Lessons 13-14):
- Run the main application inside a continuous
while Truemenu loop. - Iterate through output reports using
forloops withenumerate()and.items().
- Run the main application inside a continuous
- Pattern Matching CLI Dispatcher (Lesson 12):
- Process user CLI command tokens inside a
match-caseblock:case ["ADD_ITEM", item_id, title, price_str, stock_str]→ InstantiateInventoryItemobject and save to registry.case ["BUY", cid, item_id, qty_str]→ Fetch objects, check stock, raiseOutOfStockErrorif insufficient, and execute sale insideDatabaseSession.case ["AUDIT", "OBJECTS"]→ Inspect object instances using Class Pattern Matching.case ["EXIT" | "QUIT"]→ Terminate session safely using a sentinel flag.case _→ Output command error message.
- Process user CLI command tokens inside a
- Conditionals, Formatting & Foundations (Lessons 1-11):
- Sanitize all string inputs using
.strip()and.upper(). - Format tabular reports using f-strings with field width alignment specifiers (
:<15,:>10) and currency formatting (:,.2f).
- Sanitize all string inputs using
9. File & Workspace Directory Structure
Standard Course Workspace Layout:
Ensure all exercise files are stored inside their corresponding lesson directories:
python_mastery_course/
│
├── lesson_01/ ... lesson_31/
│
└── lesson_32/
├── class_declaration_basic.py
├── constructor_init_demo.py
├── instance_vs_class_attributes.py
├── match_case_class_pattern.py
├── inventory_class_task.py <-- (Task 1)
├── class_pattern_task.py <-- (Task 2)
└── oop_enterprise_os.py <-- (Task 3: Master Review Capstone)
OnlineCBT