# 94. Class Inheritance

**1. Simple Inheritance**

```python
class Animal:
    def speak(self):
        return "I make a sound"

class Dog(Animal):
    pass

dog = Dog()
print(dog.speak())  # Output: I make a sound
```

***

**2. Overriding Methods**

```python
class Animal:
    def speak(self):
        return "I make a sound"

class Cat(Animal):
    def speak(self):
        return "Meow"

cat = Cat()
print(cat.speak())  # Output: Meow
```

***

**3. Calling Parent Class Methods**

```python
class Animal:
    def speak(self):
        return "I make a sound"

class Bird(Animal):
    def speak(self):
        parent_speech = super().speak()
        return parent_speech + " and I chirp"

bird = Bird()
print(bird.speak())  # Output: I make a sound and I chirp
```

***

**4. Adding New Attributes in Derived Class**

```python
class Animal:
    def __init__(self, name):
        self.name = name

class Fish(Animal):
    def __init__(self, name, species):
        super().__init__(name)
        self.species = species

fish = Fish("Nemo", "Clownfish")
print(f"{fish.name} is a {fish.species}.")  # Output: Nemo is a Clownfish.
```

***

**5. Multiple Inheritance**

```python
class Flyer:
    def fly(self):
        return "I can fly"

class Swimmer:
    def swim(self):
        return "I can swim"

class Duck(Flyer, Swimmer):
    pass

duck = Duck()
print(duck.fly())   # Output: I can fly
print(duck.swim())  # Output: I can swim
```

***

**6. Checking Inheritance with `issubclass`**

```python
class Vehicle:
    pass

class Car(Vehicle):
    pass

print(issubclass(Car, Vehicle))  # Output: True
print(issubclass(Vehicle, Car))  # Output: False
```

***

**7. Polymorphism with Inherited Classes**

```python
class Animal:
    def speak(self):
        return "I make a sound"

class Dog(Animal):
    def speak(self):
        return "Woof"

class Cat(Animal):
    def speak(self):
        return "Meow"

animals = [Dog(), Cat()]
for animal in animals:
    print(animal.speak())  # Output: Woof, Meow
```

***

**8. Abstract Base Classes**

```python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

circle = Circle(5)
print(circle.area())  # Output: 78.5
```

***

**9. Method Resolution Order (MRO) in Multiple Inheritance**

```python
class A:
    def method(self):
        return "Method from A"

class B(A):
    def method(self):
        return "Method from B"

class C(A):
    pass

class D(B, C):
    pass

d = D()
print(d.method())  # Output: Method from B
```

***

**10. Private Attributes and Inheritance**

```python
class Parent:
    def __init__(self):
        self.public = "I am public"
        self._protected = "I am protected"
        self.__private = "I am private"

class Child(Parent):
    def show_attributes(self):
        print(self.public)        # Accessible
        print(self._protected)    # Accessible
        # print(self.__private)   # Not accessible (raises AttributeError)

child = Child()
child.show_attributes()
```

***

**Key Points:**

* **Inheritance** enables code reuse.
* **Overriding methods** allows for specialized behavior in derived classes.
* Use `super()` to call parent class methods.
* Python supports **multiple inheritance**, but it's essential to understand the **method resolution order (MRO)**.
* Use **abstract base classes (ABC)** to define interfaces.

These snippets demonstrate the power and flexibility of class inheritance in Python!
