import datetime

class Student:
    def __init__(self, fname, lname, idNum, classOf, dob):
        self.fname = fname
        self.lname = lname
        self.idNum = idNum
        self.classOf = classOf
        self.dob = dob
        self._age = None
        
    @property
    def classOf(self):
        return self._classOf
    
    @classOf.setter
    def classOf(self, value):
        self._classOf = value
        
    @property
    def dob(self):
        return self._dob
    
    @dob.setter
    def dob(self, value):
        self._dob = value
        today = datetime.date.today()
        self._age = today.year - self._dob.year - ((today.month, today.day) < (self._dob.month, self._dob.day))
    
    @property
    def age(self):
        return self._age
    
    def __str__(self):
        return f"Name: {self.fname} {self.lname}\nID Number: {self.idNum}\nGraduation Year: {self.classOf}\nDOB: {self.dob}\nAge: {self.age}"
        
def tester():
    s1 = Student("Vyaan", "Gautam", "1909578", "2025", datetime.date(2007, 4, 13))
    print(s1)
    s1.dob = datetime.date(2007, 4, 13)
    print(s1)
    
tester()
Name: Vyaan Gautam
ID Number: 1909578
Graduation Year: 2025
DOB: 2007-04-13
Age: None
Name: Vyaan Gautam
ID Number: 1909578
Graduation Year: 2025
DOB: 2007-04-13
Age: 15

import datetime : This line is importing the datetime module. This module provides classes for manipulating dates and times.

class Student: : This line is defining the start of the class. The class is called "Student".

def init(self, fname, lname, idNum, classOf, dob): : This is the constructor method for the class. It gets called when an object of the class is created. It takes five parameters: "fname", "lname", "idNum", "classOf" and "dob" which are all assigned to the object's corresponding instance variables.

self._age = None: This line creates an instance variable "_age" and assigns a value of None to it.

@property : This is a Python decorator, which can be added above a method to indicate that it should be treated as a property. For example, it allows you to access the method like an attribute without calling it like a function.

def classOf(self): : This is a getter method for the classOf property. It simply returns the value of the instance variable "_classOf".

@classOf.setter : This is another decorator that specifies that the following method is a setter method for the classOf property.

def classOf(self, value): : This is the setter method for the classOf property. It takes one parameter, "value", which it assigns to the instance variable "_classOf".

def dob(self): : This is a getter method for the dob property. It simply returns the value of the instance variable "_dob".

def dob(self, value): : This is the setter method for the dob property. It takes one parameter, "value", which it assigns to the instance variable "_dob". It also calculates the age of the student using today's date and assigns the result to the instance variable "_age".

def age(self): : This is a getter method for the age property. It simply returns the value of the instance variable "_age".

def str(self): : This is a special method that defines how the class should be represented as a string. It returns a formatted string that includes the student's name, ID number, graduation year, DOB, and age.

def tester(): : This is a simple test function that creates an instance of the Student class and prints it out, then modifies its birthdate and prints it out again

tester() : This is calling the tester function and run the test case