Q1Which of the following is the correct way to make Dog inherit from Animal?
Class Inheritance — Reusing Features from an Existing Class
Learn Python class inheritance. Inherit attributes and methods with class Child(Parent):, override methods, and call the parent's version with super() — all with diagrams.
Last time we used special methods like __add__ and __str__ to teach built-in operations to our own classes. This time we'll cover one of the core ideas in OOP: class inheritance.
Why Use Inheritance?
When you build apps, you often run into classes that are mostly the same but differ in a few spots. Imagine Customer, Employee, and Admin — they all hold a name and email and return a greeting(), but each one has a few unique attributes or methods on top.
You could write the same name / email / greeting() in all three classes — but the code would be duplicated and every change would have to happen in three places. The cleaner solution is to put the shared parts in a parent class and have each child class "inherit from the parent and only write the differences." That's inheritance.
Person). Only the differences go in the child (Employee). No need to redeclare name or greeting every time.Inheriting from a Parent — class Child(Parent):
The syntax is simple: when defining a subclass, put the parent class in parentheses after the class name. The subclass automatically picks up all of the parent's attributes and methods.
Below, Person is the parent (with name, age, and a greeting) and Employee is the child. Employee doesn't define a single method of its own, but calling greeting() on it just works thanks to inheritance.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greeting(self):
print(f"Hello, {self.name}")
def say_age(self):
print(f"{self.age} years old")
class Employee(Person): # inherits from Person
pass # body can be empty
e = Employee("Alice", 45)
e.greeting() # Hello, Alice <- parent's method
e.say_age() # 45 years old
- Attributes: name / age
- Methods: greeting() / say_age()
- __init__(self, name, age)
- Body is empty (just pass)
- → Inherits parent's attributes and methods
- Employee("Alice", 45) calls the parent's __init__
Method Overriding — Replacing with the Same Name
Pure inheritance gives you the parent's behavior unchanged. But if you write a method with the same name in the child class, the child's version takes priority. That's an override.
For example, to give Employee a different greeting like "Hello, Employee Alice", redefine greeting in the child. Calling it on a Person instance gives the parent's greeting; calling it on an Employee instance gives the child's. The behavior switches based on which class the calling instance belongs to.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greeting(self):
print(f"Hello, {self.name}")
class Employee(Person):
def greeting(self): # same name = override
print(f"Hello, Employee {self.name}")
p = Person("Bob", 30)
e = Employee("Alice", 45)
p.greeting() # Hello, Bob
e.greeting() # Hello, Employee Alice <- child's version wins
super() — Calling the Parent's Method
Sometimes you want to override a method but still build on the parent's behavior. That's where super() comes in. Writing super().method() calls the same-named method on the current class's parent.
For example, suppose you want "the parent's greeting (which prints Hello, Alice) followed by an extra line in the child." Inside the child's greeting, call super().greeting() first, then add your own line.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greeting(self):
print(f"Hello, {self.name}")
class Employee(Person):
def greeting(self):
super().greeting() # ① run parent's greeting first
print(f"I'm Employee {self.name}") # ② then child's own line
Employee("Alice", 45).greeting()
# Hello, Alice
# I'm Employee Alice
greeting, calling super().greeting() runs the parent's greeting directly. self is passed automatically — you don't write it.Overriding __init__ to Add More Attributes
Where super() shines most is when you override __init__. Imagine the child class needs the parent's attributes plus a few of its own — for example, Employee should also hold a phone_number.
In that case you redefine __init__ in the child, but the parent's initialization (self.name = name etc.) is handled in one line by super().__init__(name, age), and you add the extra attributes after. That way you don't have to repeat the name / age assignments.
Employee(...) runs the child's __init__ → super().__init__(name, age) lets the parent assign name / age → then the child adds phone_number. Final state: parent + child attributes all in place.class Person:
def __init__(self, name, age):
self.name = name
self.age = age
class Employee(Person):
def __init__(self, name, age, phone_number):
super().__init__(name, age) # ① parent sets name / age
self.phone_number = phone_number # ② add the child-only attr
def show(self):
print(f"{self.name} / {self.age} / {self.phone_number}")
Employee("Alice", 45, "555-1111").show()
# Alice / 45 / 555-1111
Forget super().__init__(...) and the Attributes Vanish
If you override __init__ but forget to call super().__init__(...), the parent's assignments like self.name = name never happen. The next time you read self.name, you get an AttributeError. Whenever you override __init__ in a child class, call super().__init__(...) right at the top — make it a habit.
Knowledge Check
Answer each question one by one.
Q2Which is the most accurate description of method overriding?
Q3If you forget to call super().__init__(...) in a child's __init__, what happens?