# 101. Overriding Methods

**1. Basic Method Overriding**

```python
class Animal:
    def speak(self):
        print("Animal speaks")

class Dog(Animal):
    def speak(self):
        print("Dog barks")

dog = Dog()
dog.speak()  # Output: Dog barks
```

**Explanation**: In the `Dog` subclass, we override the `speak` method to provide a custom implementation.

***

**2. Overriding with Arguments**

```python
class Vehicle:
    def start(self, engine_type):
        print(f"{engine_type} engine started")

class Car(Vehicle):
    def start(self, engine_type="Petrol"):
        print(f"Car with {engine_type} engine started")

car = Car()
car.start()  # Output: Car with Petrol engine started
car.start("Diesel")  # Output: Car with Diesel engine started
```

**Explanation**: In the `Car` subclass, we override the `start` method to specify a default argument.

***

**3. Calling the Parent Method with `super()`**

```python
class Animal:
    def speak(self):
        print("Animal speaks")

class Dog(Animal):
    def speak(self):
        super().speak()  # Call the parent class method
        print("Dog barks")

dog = Dog()
dog.speak()  # Output: Animal speaks \n Dog barks
```

**Explanation**: In `Dog`, we call the parent class's `speak` method before adding our own behavior.

***

**4. Overriding Initialization in Subclass**

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

class Student(Person):
    def __init__(self, name, age, grade):
        super().__init__(name, age)
        self.grade = grade

student = Student("John", 20, "A")
print(student.name, student.age, student.grade)  # Output: John 20 A
```

**Explanation**: The `Student` subclass overrides the `__init__` method to add a new attribute, while still calling the parent class's `__init__`.

***

**5. Overriding with Different Return Values**

```python
class Shape:
    def area(self):
        return 0

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
```

**Explanation**: The `Circle` class overrides the `area` method to compute the area of a circle.

***

**6. Overriding a Class Method**

```python
class Vehicle:
    @classmethod
    def description(cls):
        print("This is a vehicle")

class Car(Vehicle):
    @classmethod
    def description(cls):
        print("This is a car")

car = Car()
car.description()  # Output: This is a car
```

**Explanation**: We override the class method `description` in the `Car` subclass.

***

**7. Overriding a Static Method**

```python
class Animal:
    @staticmethod
    def sound():
        print("Animal makes a sound")

class Dog(Animal):
    @staticmethod
    def sound():
        print("Dog barks")

dog = Dog()
dog.sound()  # Output: Dog barks
```

**Explanation**: The static method `sound` is overridden in the `Dog` subclass to provide a specific implementation.

***

**8. Overriding Method for Logging**

```python
class Logger:
    def log(self, message):
        print(f"Log: {message}")

class FileLogger(Logger):
    def log(self, message):
        super().log(message)
        print(f"Logging to a file: {message}")

file_logger = FileLogger()
file_logger.log("This is a log message.")  # Output: Log: This is a log message. \n Logging to a file: This is a log message.
```

**Explanation**: `FileLogger` overrides `log` to add its own logging mechanism while still calling the parent class's method.

***

**9. Overriding a Method with Exception Handling**

```python
class Base:
    def calculate(self, x, y):
        return x + y

class Derived(Base):
    def calculate(self, x, y):
        try:
            return x / y
        except ZeroDivisionError:
            return "Cannot divide by zero"

derived = Derived()
print(derived.calculate(10, 2))  # Output: 5.0
print(derived.calculate(10, 0))  # Output: Cannot divide by zero
```

**Explanation**: The `Derived` class overrides the `calculate` method to handle exceptions differently.

***

**10. Overriding Methods with Different Parameters**

```python
class Animal:
    def sound(self):
        print("Animal makes a sound")

class Dog(Animal):
    def sound(self, volume="normal"):
        if volume == "loud":
            print("Dog barks loudly")
        else:
            print("Dog barks normally")

dog = Dog()
dog.sound()  # Output: Dog barks normally
dog.sound("loud")  # Output: Dog barks loudly
```

**Explanation**: The `Dog` subclass overrides the `sound` method and adds a parameter for volume control.

***

These examples demonstrate how to override inherited methods in a subclass to change their behavior, whether it's by modifying their functionality, adding new parameters, or invoking the parent class's method using `super()`.
