When we define a class we can either assign attributes to an instance of that with variable assignment or require them as part of the construction class:

class BankAccount:
	pass
 
helga = BankAccount()
helga.balance = 155
 
peter = BankAccount()
peter.owner = "Peter"
 

The issue with the above code is that we might have different BankAccount instances with different properties; the helga instance contains a balance, while the peter instance contains an owner.

We can avoid errors by defining a constructor in a class, such that the class is created if and only if all required attributes are passed:

class InvestingAccount:
	def __init__(self, balance, owner):
		self.balance = balance
		self.owner = owner

Note that the use of self here just means this instance that is being created, which is why we assign the variables balance and owner to self.balance and self.owner. In other words, the following two are equivalent in functionality:

helga = BankAccount()
helga.balance = 155
helga.owner = "helga"
 
helga = InvestingAccount(155, "helga")

The only difference is the enforcement of attributes via a the __init__ method, but the __init__ method itself assigns those variables to that instance of InvestingAccount in the same way we manually assigned them in BankAccount (although with requirements). In other words, using the constructor is like using Pydantic or other such type-enforcement rules.