OOP Complete Guide
Object-Oriented Programming (OOP) in Python: A Comprehensive Engineering Guide
In preceding modules, we focused on procedural programming, a paradigm where software is organized around procedures, functions, and sequential operations acting on separate in-memory variables. As systems grow in complexity, managing program state by passing separate variables across multiple functions becomes brittle and error-prone.
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects," which bundle both state (data, called attributes) and behavior (functions, called methods) into cohesive, self-contained units. This guide details Python's implementation of the object-oriented paradigm, spanning basic class construction to advanced encapsulation, inheritance, polymorphism, and magic method overriding.
Table of Contents
1. Paradigm Shift: Procedural vs. Object-Oriented Programming
The Procedural Bottleneck
In a procedural paradigm, data structures and the functions that manipulate them are strictly separated.
Procedural Paradigm: ┌──────────────────────┐ ┌───────────────────────┐ │ Global/Local State │ ─────► │ Manipulating Function │ │ (Separate Variables)│ │ (Processes data) │ └──────────────────────┘ └───────────────────────┘
Consider managing a student database procedurally. If you need to track a student's name, age, and scores, and update their grades, the state is stored in primitive collections (like dictionaries), and functions must consistently accept, validate, modify, and return those structures:
main.pyRunnable## procedural_approach.py def create_student(name, age, scores): return {"name": name, "age": age, "scores": scores} def calculate_average(student_dict): scores = student_dict["scores"] return sum(scores) / len(scores) if scores else 0.0 def add_score(student_dict, new_score): # Mutates the dictionary, risking unexpected side-effects across the app student_dict["scores"].append(new_score) ## Application usage student_a = create_student("Alice Johnson", 16, [90, 85, 88]) add_score(student_a, 95) avg = calculate_average(student_a) print(f"Student: {student_a['name']} | Average: {avg:.2f}")
Structural Limits of Procedural Programming
- Loose Coupling of State and Action: There is no strict enforcement link between the structure of the student dictionary and the functions that modify it. Any function can accidentally mutate key names or inject invalid types (e.g., a negative age).
- Namespace Pollution: Functions like
calculate_averageare defined in the global namespace, making name collisions likely as additional modules (such as course averages or class statistics) are introduced. - Lack of Information Hiding: The dictionary is entirely exposed; external parts of the program can directly alter or delete keys without running validation checks.
The Object-Oriented Paradigm
OOP solves these problems by merging state and behavior into a unified container called a Class. A class defines the blueprint for custom data types, enclosing the variables (attributes) and functions (methods) that operate strictly within the domain of that type.
Object-Oriented Paradigm: ┌────────────────────────────────────────────────────────┐ │ Class Object │ │ ┌────────────────────────────────────────────────┐ │ │ │ Internal Attributes (Encapsulated State) │ │ │ │ - name: "Alice Johnson" │ │ │ │ - scores: [90, 85, 88, 95] │ │ │ └────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────┐ │ │ │ Internal Methods (Behaviors) │ │ │ │ - add_score(score) │ │ │ │ - calculate_average() │ │ │ └────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────┘
Self-Check Checklist
2. Classes & Objects: Blueprints and Instances
Definitions
- Class: A user-defined prototype or blueprint that defines the structure and behavior of a custom data type. It specifies what attributes its instances will hold and what methods they can execute. Defining a class does not allocate memory for data; it only defines the type schema.
- Object (Instance): An individual instance of a class created at runtime. When you instantiate a class, Python allocates memory to store the object's distinct attributes on the heap.
main.pyRunnable## defining_classes.py class AcademicCourse: """Represents an academic course within a school registry""" pass ## Instantiating objects (creating instances of AcademicCourse) course_physics = AcademicCourse() course_chemistry = AcademicCourse() print(f"Class blueprint type: {AcademicCourse}") print(f"Physics instance: {course_physics} | Type: {type(course_physics)}") print(f"Chemistry instance: {course_chemistry} | Type: {type(course_chemistry)}")
Output:
Class blueprint type: <class '__main__.AcademicCourse'> Physics instance: <__main__.AcademicCourse object at 0x7f81b8> | Type: <class '__main__.AcademicCourse'> Chemistry instance: <__main__.AcademicCourse object at 0x7f81c0> | Type: <class '__main__.AcademicCourse'>
Each instantiated object resides at a unique hexadecimal memory address on the heap, ensuring that their individual states remain completely isolated from one another.
Self-Check Checklist
3. Anatomy of a Class: Attributes & Methods
Attributes: Instance vs. Class Variables
A class holds two distinct categories of attributes:
- Instance Variables: Variables defined within methods (most commonly inside the constructor) that belong to a specific object. Each instance maintains its own isolated copy of these variables.
- Class Variables (Static Variables): Variables declared directly within the class body (outside of any methods). They are shared by all instances of the class and can be accessed via the class itself.
main.pyRunnable## variables_scope.py class RegisteredStudent: # Class Variable: Shared across all instances of this class school_name = "Grandview Academy" total_enrolled = 0 def __init__(self, name): # Instance Variable: Unique to each instance self.name = name # Modify the class variable globally via class name reference RegisteredStudent.total_enrolled += 1 ## Instantiate instances student_a = RegisteredStudent("Alice") student_b = RegisteredStudent("Bob") ## Accessing Instance Variables (Isolated states) print(f"Student A Name: {student_a.name}") print(f"Student B Name: {student_b.name}") ## Accessing Class Variables (Shared state) print(f"Student A School: {student_a.school_name}") print(f"Student B School: {student_b.school_name}") print(f"Total Enrollment Count: {RegisteredStudent.total_enrolled}")
Output:
Student A Name: {student_a.name} -> Alice Student B Name: {student_b.name} -> Bob Student A School: Grandview Academy Student B School: Grandview Academy Total Enrollment Count: 2
Methods: The self Parameter
Methods are functions defined inside a class that operate on the attributes of an object. The first parameter of an instance method is always named self.
Instance Method Execution Pipeline: Call: student_a.introduce() │ ▼ Python executes behind the scenes: RegisteredStudent.introduce(student_a)
The self parameter is a reference to the active object instance calling the method. Python automatically passes the instance reference as the first argument, allowing you to access or modify the object's instance variables dynamically.
main.pyRunnable## instance_methods.py class RegisteredStudent: def __init__(self, name, grade): self.name = name self.grade = grade def update_grade(self, new_grade): """Instance method modifying the object's state""" self.grade = new_grade print(f"[System] Grade updated for {self.name} to {self.grade}") student_a = RegisteredStudent("Alice Johnson", 11) ## Call method (self is implicitly passed as student_a) student_a.update_grade(12)
Self-Check Checklist
4. Constructors & State Initialization
The __init__ Method
The __init__ method is Python's instance initialization method (often referred to as the constructor). When you instantiate a class (e.g., student = Student()), Python automatically creates an empty object in memory and then immediately calls __init__, passing the new object reference as the first argument (self), followed by any additional arguments passed during instantiation.
Input Type Validation & Default Arguments
To build robust applications, you must validate all inputs passed to the constructor. This prevents invalid data types or invalid ranges from corrupting the object's initial state.
main.pyRunnable## strict_initialization.py class CourseEnrollment: def __init__(self, student_name, grade_level=9): """Initializes state with strict parameter validation""" # Type Validation if not isinstance(student_name, str) or not student_name.strip(): raise TypeError("student_name must be a non-empty string.") # Range Validation if not isinstance(grade_level, int) or not (9 <= grade_level <= 12): raise ValueError("grade_level must be an integer between 9 and 12.") # Assign verified attributes self.student_name = student_name.strip() self.grade_level = grade_level self.completed_credits = 0.0 try: # Attempt instantiation with invalid grade_level invalid_student = CourseEnrollment("Bob Smith", 14) except ValueError as error: print(f"Validation Blocked Instantiation: {error}") ## Successful instantiation using default argument valid_student = CourseEnrollment("Alice Johnson") print(f"Student: {valid_student.student_name} | Grade: {valid_student.grade_level}")
Output:
Validation Blocked Instantiation: grade_level must be an integer between 9 and 12. Student: Alice Johnson | Grade: 9
Self-Check Checklist
5. Encapsulation & Data Hiding
Public, Protected, and Private Members
Encapsulation is the practice of restricting direct access to an object's internal state. In Python, this data hiding is indicated using variable naming conventions:
- Public Attributes (e.g.,
self.name): Can be accessed and modified directly from anywhere outside the class. - Protected Attributes (e.g.,
self._name): Indicated by a single leading underscore. This is a convention warning external developers that the attribute is intended for internal use within the class and its subclasses. It is not syntactically blocked by Python. - Private Attributes (e.g.,
self.__name): Indicated by a double leading underscore. Python syntactically hides these attributes from direct external modification.
Name Mangling
When an attribute is prefixed with a double underscore, Python's parser automatically renames it internally to _ClassName__attributeName. This process, called Name Mangling, prevents direct access or overrides from outside the class.
main.pyRunnable## name_mangling_demo.py class SecureStudentLocker: def __init__(self, owner, secret_combination): self.owner = owner # Public self._locker_status = "assigned" # Protected self.__secret_combination = secret_combination # Private locker = SecureStudentLocker("Alice", "44-22-11") print(f"Public: {locker.owner}") print(f"Protected: {locker._locker_status}") try: # Direct access to private attribute will raise an AttributeError print(locker.__secret_combination) except AttributeError as error: print(f"Direct Private Access Blocked: {error}") ## Accessing private attribute using its mangled name (highly discouraged in production) mangled_val = locker._SecureStudentLocker__secret_combination print(f"Mangled Access: {mangled_val}")
Output:
Public: Alice Protected: assigned Direct Private Access Blocked: 'SecureStudentLocker' object has no attribute '__secret_combination' Mangled Access: 44-22-11
Getter and Setter Properties
Instead of exposing attributes directly, you can control access to them using Python's @property decorator. This allows you to expose private attributes safely, validating new values before applying changes.
main.pyRunnable## property_encapsulation.py class AcademicGrade: def __init__(self, student_name, initial_score): self.student_name = student_name self.__score = None # Use the setter property to apply initial value validation self.score = initial_score @property def score(self): """Getter property: Exposes the private __score attribute""" return self.__score @score.setter def score(self, value): """Setter property: Validates value before mutating state""" if not isinstance(value, (int, float)): raise TypeError("Score must be a numeric value.") if not (0.0 <= value <= 100.0): raise ValueError("Score must range between 0.0 and 100.0.") self.__score = float(value) ## Usage grade_record = AcademicGrade("Alice", 85) print(f"Grade: {grade_record.score}%") try: # Attempting to assign invalid value triggers validation grade_record.score = 110.0 except ValueError as error: print(f"Mutation Denied: {error}")
Output:
Grade: 85.0% Mutation Denied: Score must range between 0.0 and 100.0.
Self-Check Checklist
6. Inheritance & Code Hierarchies
Subclassing & Constructor Chaining (super())
Inheritance allows you to define a new class (derived or child class) that inherits the attributes and methods of an existing class (base or parent class). The child class can extend or modify the inherited behavior.
When instantiating a child class, you must invoke the parent class's constructor using super().__init__() to ensure that parent attributes are initialized correctly before the child class applies its own distinct initializations.
main.pyRunnable## constructor_chaining.py class SchoolMember: """Parent base class representing any member in the school domain""" def __init__(self, name, member_id): self.name = name self.member_id = member_id def get_details(self): return f"{self.name} (ID: {self.member_id})" class SubClassStudent(SchoolMember): """Child derived class inheriting from SchoolMember""" def __init__(self, name, member_id, grade_level): # Chain constructor call to initialize parent attributes super().__init__(name, member_id) # Initialize child specific attributes self.grade_level = grade_level def get_details(self): """Method Overriding: Customizes details specifically for students""" base_details = super().get_details() return f"Student: {base_details} | Grade Level: {self.grade_level}" member = SubClassStudent("Alice Johnson", "S101", 10) print(member.get_details())
Output:
Student: Alice Johnson (ID: S101) | Grade Level: 10
Method Resolution Order (MRO) & Multiple Inheritance
Python supports Multiple Inheritance, which allows a class to inherit from multiple parent classes.
Multiple Inheritance & Diamond Problem MRO Flow: ┌─────────────┐ │ Class A │ └──────┬──────┘ │ ┌──────┴──────┐ │ Class B │ └──────┬──────┘ │ ┌──────┴──────┐ │ Class C │ (Overrides method) └──────┬──────┘ │ ┌──────▼──────┐ │ Class D │ (Inherits from B and C) └─────────────┘
When multiple parent classes contain methods with the same name, Python resolves which method to execute using a strict ordering called the Method Resolution Order (MRO), based on the C3 Linearization algorithm.
main.pyRunnable## multiple_inheritance_mro.py class Device: def boot(self): return "Device booting..." class Charger: def boot(self): return "Charger powering up..." class SmartBoard(Device, Charger): """Inherits from both Device and Charger""" pass board = SmartBoard() ## Executes the boot method from Device, as it is listed first in inheritance order print(board.boot()) print(f"MRO Order: {SmartBoard.__mro__}")
Output:
Device booting... MRO Order: (<class '__main__.SmartBoard'>, <class '__main__.Device'>, <class '__main__.Charger'>, <class 'object'>)
Self-Check Checklist
7. Polymorphic Architecture & Duck Typing
Interface Uniformity
Polymorphism is the ability of different classes to respond to the same method call with distinct, subclass-specific behaviors. This allows you to write generic, polymorphic code that processes a variety of related object types uniformly, without needing to verify their concrete classes.
main.pyRunnable## polymorphic_interfaces.py class BaseAcademicEvaluator: def evaluate(self): raise NotImplementedError("Subclasses must implement abstract evaluate method.") class ExamEvaluator(BaseAcademicEvaluator): def evaluate(self): return "Evaluating paper exams with grading rubrics." class LabEvaluator(BaseAcademicEvaluator): def evaluate(self): return "Evaluating practical chemistry experiments in the lab." def process_evaluation(evaluator_object): """Polymorphic helper function that accepts any evaluator class""" # Executes the subclass-specific evaluate method dynamically print(evaluator_object.evaluate()) evaluators = [ExamEvaluator(), LabEvaluator()] for e in evaluators: process_evaluation(e)
Output:
Evaluating paper exams with grading rubrics. Evaluating practical chemistry experiments in the lab.
Duck Typing
In Python, polymorphism is not restricted to formal inheritance hierarchies. Python relies heavily on Duck Typing ("If it walks like a duck and quacks like a duck, it's a duck").
If two completely unrelated classes implement the same method interface, they can be used interchangeably in polymorphic contexts, without needing to inherit from a shared parent class.
main.pyRunnable## duck_typing_demo.py class SchoolBell: def ring(self): return "CLANG! School bell rings to dismiss students." class Telephone: def ring(self): return "Ringtone: Phone incoming connection." def trigger_sound(ringer_object): """Executes ring on any object that provides the ring interface""" print(ringer_object.ring()) ## Unrelated objects processed through the same interface trigger_sound(SchoolBell()) trigger_sound(Telephone())
Output:
CLANG! School bell rings to dismiss students. Ringtone: Phone incoming connection.
Self-Check Checklist
8. Magic (Dunder) Methods & Operator Overloading
What are Magic Methods?
Magic methods (often called Dunder Methods because they begin and end with double underscores, such as __init__) are special methods that allow you to define how your custom objects behave in response to Python's built-in operations, operators, and syntax structures.
Standard Magic Overrides
Here are some of the most common magic methods used to customize object behavior:
__str__(self): Defines a user-friendly string representation of the object (returned bystr()andprint()).__repr__(self): Defines an unambiguous, developer-focused representation of the object (returned byrepr()).__len__(self): Customizes what is returned when the object is passed tolen().__add__(self, other): Overloads the+operator to define custom addition behavior between objects.__eq__(self, other): Overloads the==comparison operator to define when two objects are considered equal.
main.pyRunnable## magic_methods_overload.py class StudentCohort: def __init__(self, name, grade): self.name = name self.grade = grade self.students = [] def enroll(self, student_name): self.students.append(student_name) def __str__(self): """User-friendly string representation""" return f"Cohort '{self.name}' ({len(self.students)} students enrolled)" def __repr__(self): """Technical developer representation""" return f"StudentCohort(name='{self.name}', grade={self.grade})" def __len__(self): """Returns student headcount when len() is called""" return len(self.students) def __add__(self, other_cohort): """Combines student rosters when two cohorts are added (+)""" if not isinstance(other_cohort, StudentCohort): raise TypeError("Can only add another StudentCohort.") combined = StudentCohort(f"{self.name} + {other_cohort.name}", self.grade) combined.students = self.students + other_cohort.students return combined def __eq__(self, other_cohort): """Compares equality based on the number of enrolled students""" if not isinstance(other_cohort, StudentCohort): return False return len(self.students) == len(other_cohort.students) ## Usage cohort_a = StudentCohort("Physics A", 11) cohort_a.enroll("Alice") cohort_a.enroll("Bob") cohort_b = StudentCohort("Physics B", 11) cohort_b.enroll("Charlie") ## Test Dunder Methods print(f"str() invocation: {cohort_a}") print(f"repr() invocation: {repr(cohort_a)}") print(f"len() invocation: {len(cohort_a)}") ## Operator Overloading (+) cohort_combined = cohort_a + cohort_b print(f"Combined Cohort: {cohort_combined}") print(f"Roster: {cohort_combined.students}") ## Comparison Overloading (==) print(f"Is Cohort A equal in size to Cohort B? {cohort_a == cohort_b}") cohort_b.enroll("David") # Now both have 2 students print(f"Is Cohort A equal in size to Cohort B now? {cohort_a == cohort_b}")
Output:
str() invocation: Cohort 'Physics A' (2 students enrolled) repr() invocation: StudentCohort(name='Physics A', grade=11) len() invocation: 2 Combined Cohort: Cohort 'Physics A + Physics B' (3 students enrolled) Roster: ['Alice', 'Bob', 'Charlie'] Is Cohort A equal in size to Cohort B? False Is Cohort A equal in size to Cohort B now? True
Self-Check Checklist
9. Comprehensive Capstone Project: Academic Registry & Catalog System
This capstone project implements an object-oriented Academic Registry and Catalog System. It showcases encapsulation, property verification, class composition, inheritance, polymorphic behaviors, and file-based data serialization using a clean, professional OOP architecture.
Class Composition Diagram
┌──────────────────┐ │ SchoolMember │ (Base Parent Class) └────────┬─────────┘ │ ┌──────────────┴──────────────┐ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Student │ │ Teacher │ (Subclasses) └──────┬───────┘ └──────┬───────┘ │ (Composition) │ (Composition) ▼ ▼ ┌────────────────────────────────────────────┐ │ AcademicCourse │ (Manages rosters) └────────────────────┬───────────────────────┘ ▼ (Serialization) ┌────────────────────────────────────────────┐ │ RegistryDatabaseManager │ (JSON backup engine) └────────────────────────────────────────────┘
Registry System Implementation
main.pyRunnable## academic_registry_system.py import json import pathlib ## ===================================================================== ## 1. BASE CLASS & INHERITANCE HIERARCHY ## ===================================================================== class SchoolMember: """Represents a member within the school district community""" def __init__(self, name, member_id, email): if not name or not isinstance(name, str): raise TypeError("Name must be a non-empty string.") if not member_id or not isinstance(member_id, str): raise TypeError("Member ID must be a non-empty string.") self.name = name.strip() self.member_id = member_id.strip() self.email = email.strip() def get_role_description(self): """Polymorphic base signature""" return "General School District Member" class Student(SchoolMember): """Represents an enrolled student in the district""" def __init__(self, name, member_id, email, grade_level): super().__init__(name, member_id, email) # Setter validation will apply to initial grade assignment self.grade_level = grade_level self.__gpa = 4.0 @property def grade_level(self): return self.__grade_level @grade_level.setter def grade_level(self, value): if not isinstance(value, int) or not (9 <= value <= 12): raise ValueError("Grade level must be an integer between 9 and 12.") self.__grade_level = value @property def gpa(self): return self.__gpa @gpa.setter def gpa(self, value): if not isinstance(value, (int, float)) or not (0.0 <= value <= 4.0): raise ValueError("GPA must range between 0.0 and 4.0.") self.__gpa = float(value) def get_role_description(self): """Overrides base behavior polymorphically""" return f"Student (Grade {self.grade_level} | GPA: {self.gpa:.2f})" class Teacher(SchoolMember): """Represents a faculty teacher in the district""" def __init__(self, name, member_id, email, department): super().__init__(name, member_id, email) if not department or not isinstance(department, str): raise TypeError("Department must be a non-empty string.") self.department = department.strip() def get_role_description(self): """Overrides base behavior polymorphically""" return f"Faculty Teacher ({self.department} Department)" ## ===================================================================== ## 2. CLASS COMPOSITION & DATA COLLECTION ## ===================================================================== class AcademicCourse: """Manages an academic course roster, combining Student and Teacher objects""" def __init__(self, course_name, course_code, max_capacity=3): self.course_name = course_name self.course_code = course_code self.max_capacity = max_capacity self.instructor = None self.__roster = [] # Private list composing Student instances def assign_instructor(self, teacher): """Assigns an instructor, validating they are a Teacher instance""" if not isinstance(teacher, Teacher): raise TypeError("Only a valid Teacher instance can instruct a course.") self.instructor = teacher print(f"[Course] Assigned instructor {teacher.name} to {self.course_name}") def enroll_student(self, student): """Enrolls a student, validating they are a Student instance and capacity permits""" if not isinstance(student, Student): raise TypeError("Only a valid Student instance can be enrolled.") if len(self.__roster) >= self.max_capacity: print(f"[Warning] Cannot enroll {student.name}: Course {self.course_code} is full.") return False self.__roster.append(student) print(f"[Course] Enrolled student {student.name} in {self.course_code}") return True def get_roster(self): """Returns a read-only list copy of the private student roster""" return list(self.__roster) def print_course_details(self): """Prints details of the course using polymorphic methods""" print(f"\nCourse: {self.course_name} ({self.course_code})") print(f"Instructor: {self.instructor.name if self.instructor else 'Unassigned'}") print(f"Current Class Size: {len(self.__roster)}/{self.max_capacity}") print("Roster Status:") for idx, student in enumerate(self.__roster, 1): # Calls the polymorphic get_role_description method from Student print(f" {idx}. {student.name} - {student.get_role_description()}") ## ===================================================================== ## 3. SERIALIZATION ENGINE (JSON BACKUP) ## ===================================================================== class RegistryDatabaseManager: """Handles saving and loading course data to/from disk using JSON serialization""" def __init__(self, backup_file_path): self.backup_path = pathlib.Path(backup_file_path) def save_course_data(self, course): """Serializes course details and its student roster to a JSON file""" if not isinstance(course, AcademicCourse): raise TypeError("Can only save AcademicCourse instances.") roster_data = [] for student in course.get_roster(): roster_data.append({ "name": student.name, "member_id": student.member_id, "email": student.email, "grade_level": student.grade_level, "gpa": student.gpa }) backup_payload = { "course_name": course.course_name, "course_code": course.course_code, "max_capacity": course.max_capacity, "instructor": { "name": course.instructor.name, "member_id": course.instructor.member_id, "email": course.instructor.email, "department": course.instructor.department } if course.instructor else None, "roster": roster_data } with open(self.backup_path, "w", encoding="utf-8") as file: json.dump(backup_payload, file, indent=4) print(f"\n[System] Course data successfully saved to {self.backup_path}") def load_course_data(self): """Loads and reconstructs an AcademicCourse instance from JSON""" if not self.backup_path.exists(): return None with open(self.backup_path, "r", encoding="utf-8") as file: payload = json.load(file) # 1. Reconstruct Course instance course = AcademicCourse( payload["course_name"], payload["course_code"], payload["max_capacity"] ) # 2. Reconstruct Instructor if assigned ins_data = payload.get("instructor") if ins_data: teacher = Teacher( ins_data["name"], ins_data["member_id"], ins_data["email"], ins_data["department"] ) course.assign_instructor(teacher) # 3. Reconstruct Student roster for s_data in payload.get("roster", []): student = Student( s_data["name"], s_data["member_id"], s_data["email"], s_data["grade_level"] ) student.gpa = s_data["gpa"] course.enroll_student(student) print(f"[System] Course data successfully loaded from {self.backup_path}") return course ## ===================================================================== ## 4. SYSTEM RUNTIME VERIFICATION ## ===================================================================== if __name__ == "__main__": db_file = "backup_physics.json" # Clean up any existing backup file backup_path = pathlib.Path(db_file) if backup_path.exists(): backup_path.unlink() # 1. Instantiate Core Faculty and Students teacher_jones = Teacher("Dr. Alan Jones", "T801", "ajones@school.edu", "Physics") student_alice = Student("Alice Johnson", "S101", "alice@school.edu", 11) student_alice.gpa = 3.9 student_bob = Student("Bob Smith", "S102", "bob@school.edu", 12) student_bob.gpa = 3.6 # 2. Create Course and Assign Roster (Composition) physics_course = AcademicCourse("Advanced AP Physics", "PHYS-201", max_capacity=2) physics_course.assign_instructor(teacher_jones) physics_course.enroll_student(student_alice) physics_course.enroll_student(student_bob) # 3. Trigger capacity warning limit student_charlie = Student("Charlie Davis", "S103", "charlie@school.edu", 10) physics_course.enroll_student(student_charlie) # 4. Print Course Details before saving physics_course.print_course_details() # 5. Serialize Course to Disk db_manager = RegistryDatabaseManager(db_file) db_manager.save_course_data(physics_course) # 6. Reconstruct Course from JSON Backup and Print Reconstructed Details reconstructed_course = db_manager.load_course_data() reconstructed_course.print_course_details() # Clean up created file if backup_path.exists(): backup_path.unlink()
Output:
[Course] Assigned instructor Dr. Alan Jones to Advanced AP Physics [Course] Enrolled student Alice Johnson in PHYS-201 [Course] Enrolled student Bob Smith in PHYS-201 [Warning] Cannot enroll Charlie Davis: Course PHYS-201 is full. Course: Advanced AP Physics (PHYS-201) Instructor: Dr. Alan Jones Current Class Size: 2/2 Roster Status: 1. Alice Johnson - Student (Grade 11 | GPA: 3.90) 2. Bob Smith - Student (Grade 12 | GPA: 3.60) [System] Course data successfully saved to backup_physics.json [Course] Assigned instructor Dr. Alan Jones to Advanced AP Physics [Course] Enrolled student Alice Johnson in PHYS-201 [Course] Enrolled student Bob Smith in PHYS-201 [System] Course data successfully loaded from backup_physics.json Course: Advanced AP Physics (PHYS-201) Instructor: Dr. Alan Jones Current Class Size: 2/2 Roster Status: 1. Alice Johnson - Student (Grade 11 | GPA: 3.90) 2. Bob Smith - Student (Grade 12 | GPA: 3.60)
Self-Check Checklist
Finished this lesson?
Mark it complete to advance your curriculum progress.