Python does not care about checking types, inheritance relationships, etc. ahead of time. It only cares what happens when the code is run. This allows for what is called "duck typing":
class Employee:
def fired(self):
print("Oh no, I have to look for a new job")
class Gun:
def fired(self):
print("Bang! Did the bullet hit the target?")
for thing in [Employee(), Gun()]:
thing.fired()
It does not matter that the classes Employee
and Gun
have nothing to do with each other, and it does not matter that the purpose of fired
is completely different. Both of the objects in the list have a method that is named fired
, so the code works without error.
Similarly in your code:
class emp:
def __init__(self,fname,lname,empno):
self.empno=empno
person.__init__(self,fname,lname)
If we call person.__init__
, then that is just a function that we found inside the person
class - not a method that we looked up on an object. This is because we used a class on the left-hand side of .
, not an instance. That function will happily accept an emp
instance as the value for self
- it does not care whether that self
is a person
instance. It's a user-defined object, so there is no problem setting fname
and lname
attributes.
After that, the printper
method will have no problem finding fname
and lname
attributes on the emp
instance - because they are there.
"So why bother ever inheriting at all, then?"
Because even though this "works", it still doesn't actually make an inheritance relationship. That has some consequences:
super
does not work - the __init__
call had to say explicitly what other class to use, and the code doesn't really make sense. From an outside perspective, it's sheer coincidence that it sets the right attributes.
isinstance
will not work, by default. emp
is not in the __bases__
, nor the __mro__
of person
.