O.O. Teacher List

After the feedback I got on my previous question, I decided to try one more program to see if I understand O.O.P. The idea for this is also based off the project I am working on im my class for this semester which is to make an interactive school tour.

This program just has you make a bunch of teacher instances and then prints them all out.

class Teacher:
def __init__(self, name, subject, room_number):
self.name = name
self.subject = subject
self.room_number = room_number
def print_info(self):
return self.name, self.subject, self.room_number

def instagator(teachers: list) -> list:
"""Creates a version of the teacher class"""
print("")
name = input("Please enter a teachers name: ")
subject = input("Please enter that teachers subject: ")
room_number = input("Please enter what room that teacher teaches in: ")
# Makes the varible that holds the instance names after the teacher
globals()[name] = Teacher(name, subject, room_number)
teachers.append(globals()[name])
return teachers

teachers = []
print("Please enter the name of 10 teachers, the subject they teach, and the room they teach that subject in.")
for _ in range(10):
teachers = instagator(teachers)
print("\n")
for teacher in teachers:
info = teacher.print_info()
print(f"{info[0].capitalize()} teaches {info[1]} in room {info[2]}. \n")



As with before I just want to know if there is anything wrong with my approach to O.O.P. as I still have very little idea of what I am doing.

1. Is the user of your Teacher class supposed to be able to modify the name, subject, and room_number attributes after they're set? If not, make them private by putting a _ at the start of those names.

2. Python objects have a magic method __repr__ that turns them into a string for printing. That would be a good place to put your formatting logic.

3. I don't know what this stuff with globals is trying to do, but as a general rule you should not touch globals.

4. Your instagator function says that it creates a Teacher, but it also takes a list of the existing teachers, appends to it, and then returns it. Try to have your functions do one obvious thing instead of doing multiple non-obvious things.

If your instagator function just does the one thing it says it does (creates a Teacher), and if you move the string formatting into the __repr__ function instead of having an intermediate print_info that doesn't actually print anything, the code gets a bit simpler:

class Teacher:
def __init__(self, name: str, subject: str, room_number: str):
self._name = name
self._subject = subject
self._room_number = room_number

def __repr__(self) -> str:
return f"{self._name.capitalize()} teaches {self._subject} in room {self._room_number}."

def instagator() -> Teacher:
"""Creates a Teacher from user input."""
print("")
name = input("Please enter a teachers name: ")
subject = input("Please enter that teachers subject: ")
room_number = input("Please enter what room that teacher teaches in: ")
return Teacher(name, subject, room_number)

print("Please enter the name of 10 teachers, the subject they teach, and the room they teach that subject in.")
teachers = [instagator() for _ in range(10)]
print("\n")

for teacher in teachers:
print(teacher)


Note that since instagator returns a Teacher I can just use a simple list comprehension to build a list of all the Teachers, one for each number in the range. And since I implemented __repr__ I don't need to have multiple lines of code to build the string to print; I can just print the teacher directly and that will automagically turn into the formatted string that I want.

I don't think it's great practice in general to have a constructor prompt for user input, but for a practice exercise like this I think it's fine to demonstrate how you can use a class to encapsulate all of the logic that pertains to building an object:

class Teacher:
def __init__(self):
"""Creates a Teacher from user input."""
print("")
self._name = input("Please enter a teachers name: ")
self._subject = input("Please enter that teachers subject: ")
self._room_number = input("Please enter what room that teacher teaches in: ")

def __repr__(self) -> str:
return f"{self._name.capitalize()} teaches {self._subject} in room {self._room_number}."

print("Please enter the name of 10 teachers, the subject they teach, and the room they teach that subject in.")
teachers = [Teacher() for _ in range(10)]
print("\n")

for teacher in teachers:
print(teacher)

• The globals() command is to make it so the name of the variable the instance is assigned to is the name of the teacher. I am not entirely sure how it works, but I could not find any better way of doing this. – K00lman Mar 3 at 1:56
• You should not do this at all. There's no need to have named variables for these values at the global scope, especially since you never do anything with those values after you've set them (that's why the purpose was unclear to me). Global variables in general should be avoided, and defining them dynamically in random places is almost never going to be a good idea. – Samwise Mar 3 at 3:36
• You don't want to implement __repr__, you want to implement __str__. They are similar, but different. Roughly speaking: __repr__ is designed for debugging, and perhaps serialization; __str__ is for human-readable output. See str -vs- repr. – AJNeufeld Mar 3 at 23:12

Maybe you have not gotten this far in your study, but there are actually solutions for this within OOP.

Alternative constructors can be done in Python by using a classmethod (since function overloading is not so easy). Classmethods take as a first argument the class instead of an instance (customarily called cls instead of self) and return an instance of that class. You can create them by decorating a method with @classmethod.

Keeping track of all instances created can be done by having a mutable class attribute which gets updated in the __init__ method.

In addition I also removed your print_info method and added a __str__ method which prints what you need later, instead. The __str__ method is called whenever you do str(x), which happens internally also in the print function. The __str__ and __repr__ methods are similar, but not the same. The former is for when you want a nice human-readable visualization of the instance, and the latter should be a complete representation of the instance, ideally such that eval(repr(x)) == x. For more information read this.

class Teacher:
all_teachers = {}

def __init__(self, name, subject, room_number):
self.name = name
self.subject = subject
self.room_number = room_number
self.all_teachers[name] = self

@classmethod
def from_user(cls):
"""Interactive creation of a teacher."""
print("")
name = input("Please enter a teachers name: ")
subject = input("Please enter that teachers subject: ")
room_number = input("Please enter what room that teacher teaches in: ")
return cls(name, subject, room_number)

def __str__(self):
return f"{self.name.capitalize()} teaches {self.subject} in room {self.room_number}"

if __name__ == "__main__":
print("Please enter the name of 10 teachers, the subject they teach, and the room they teach that subject in.")
teachers = [Teacher.from_user() for _ in range(10)]

for teacher in teachers:
print(teacher)
print()


The dictionary containing all teachers was not even needed, but you can access it via Teacher.all_teachers or teacher.all_teachers, where teacher is an instance.

Instead I used a list comprehension to build a list of teachers.

I also added a if __name__ == "__main__": guard to allow importing from this script without it running.