Course Topics
Python Basics Introduction and Setup Syntax and Indentation Comments and Documentation Running Python Programs Exercise Variables and Data Types Variables and Assignment Numbers (int, float, complex) Strings and Operations Booleans and None Type Conversion Exercise Operators Arithmetic Operators Comparison Operators Logical Operators Assignment Operators Bitwise Operators Exercise Input and Output Getting User Input Formatting Output Print Function Features Exercise Control Flow - Conditionals If Statements If-Else Statements Elif Statements Nested Conditionals Exercise Control Flow - Loops For Loops While Loops Loop Control (break, continue) Nested Loops Exercise Data Structures - Lists Creating and Accessing Lists List Methods and Operations List Slicing List Comprehensions Exercise Data Structures - Tuples Creating and Accessing Tuples Tuple Methods and Operations Tuple Packing and Unpacking Exercise Data Structures - Dictionaries Creating and Accessing Dictionaries Dictionary Methods and Operations Dictionary Comprehensions Exercise Data Structures - Sets Creating and Accessing Sets Set Methods and Operations Set Comprehensions Exercise Functions Defining Functions Function Parameters and Arguments Return Statements Scope and Variables Lambda Functions Exercise String Manipulation String Indexing and Slicing String Methods String Formatting Regular Expressions Basics Exercise File Handling Opening and Closing Files Reading from Files Writing to Files File Modes and Context Managers Exercise Error Handling Understanding Exceptions Try-Except Blocks Finally and Else Clauses Raising Custom Exceptions Exercise Object-Oriented Programming - Classes Introduction to OOP Creating Classes and Objects Instance Variables and Methods Constructor Method Exercise Object-Oriented Programming - Advanced Inheritance Method Overriding Class Variables and Methods Static Methods Exercise Modules and Packages Importing Modules Creating Custom Modules Python Standard Library Installing External Packages Exercise Working with APIs and JSON Making HTTP Requests JSON Data Handling Working with REST APIs Exercise Database Basics Introduction to Databases SQLite with Python CRUD Operations Exercise Final Project Project Planning Building Complete Application Code Organization Testing and Debugging Exercise

Inheritance

Introduction

In this lesson, we will delve into the concept of inheritance in Python. This topic is essential for understanding how to create complex and reusable code by building relationships between classes and objects. By the end of this tutorial, you'll learn about:

  • Creating child classes that inherit properties from parent classes
  • Overriding methods and attributes in child classes
  • Polymorphism with inheritance

Core Concepts

Inheritance allows us to create a new class based on an existing one, inheriting its properties, methods, and behavior. The existing class acts as a parent class, while the new class is called the child class. Here's a simple example:

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

    def make_sound(self):
        print("The animal makes a sound.")

class Dog(Animal):
    def make_sound(self):
        print("Woof!")

# Instantiate a dog and call its method
my_dog = Dog("Rex")
my_dog.make_sound()  # Output: Woof!

In this example, the Dog class inherits from the Animal class, and we have overridden the make_sound() method in the Dog class to print a specific sound that dogs make. This is known as method overriding.

Practical Examples

Let's create a more complex example using inheritance to model a simple hierarchy of shapes in a graphics library.

class Shape:
    def __init__(self, color):
        self.color = color

    def draw(self):
        print("Drawing a shape with color:", self.color)

class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)  # Call the parent class's constructor
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, color, radius):
        super().__init__(color)  # Call the parent class's constructor
        self.radius = radius

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

# Create and draw shapes
rectangle = Rectangle("red", 5, 10)
circle = Circle("blue", 7)

rectangle.draw()  # Output: Drawing a shape with color: red
circle.draw()     # Output: Drawing a shape with color: blue
print("Rectangle area:", rectangle.area())  # Output: Rectangle area: 50
print("Circle area:", circle.area())       # Output: Circle area: 153.87632307692746

In this example, we have created a Shape parent class with basic properties and behavior for drawing shapes. Then, we have created two child classes, Rectangle and Circle, which inherit from the Shape class and add their specific properties (width/height for Rectangle, radius for Circle) and methods (area() method for both child classes).

Common Issues and Solutions

NameError

What causes it: When you try to use a variable or function that hasn't been defined yet in the current scope.

class Dog:
    def make_sound():  # Missing `self` argument
        print("Woof!")

# Instantiating and calling the method will cause NameError
my_dog = Dog()
my_dog.make_sound()

Error message:

Traceback (most recent call last):
  File "example.py", line 5, in <module>
    my_dog.make_sound()
NameError: name 'self' is not defined

Solution: Add the self argument to the method definition.

class Dog:
    def make_sound(self):  # Correct definition with `self` argument
        print("Woof!")

Why it happens: The self variable represents the current instance of the class and is required for all methods to have access to the object's attributes.

How to prevent it: Always include the self argument in method definitions when writing new classes.

TypeError

What causes it: When you try to use a method or property that doesn't exist in the current class, but its parent class has that method/property with a different implementation.

class Animal:
    def make_sound(self):
        print("The animal makes a sound.")

class Dog(Animal):
    pass  # No overriding of the `make_sound()` method in the Dog class

my_dog = Dog()
my_dog.make_sound()  # Output: The animal makes a sound. (Even though we want it to print "Woof!")

Error message:

Traceback (most recent call last):
  File "example.py", line X, in <module>
    my_dog.make_sound()
TypeError: can't pickle local object 'Dog'

Solution: Override the method in the child class to provide a specific implementation.

class Dog(Animal):
    def make_sound(self):
        print("Woof!")

Why it happens: When you inherit from a parent class, all its methods and properties become available in the child class unless explicitly overridden or removed.

How to prevent it: Be mindful of which methods and properties you override in the child classes to ensure they have the desired behavior.

Best Practices

  • Use descriptive names for parent and child classes, making their relationship clear
  • Implement the __init__() method in both the parent and child classes to set initial values for attributes
  • Override methods in the child classes as needed to provide specialized behavior or override inherited implementations
  • Follow the Rule of Three (or Four) when overriding special methods such as __init__, __del__, __str__, etc.

Key Takeaways

  • Inheritance allows you to create child classes based on existing parent classes, inheriting their properties and methods
  • Method overriding allows you to customize the behavior of inherited methods in child classes
  • Polymorphism with inheritance enables objects of different types (child classes) to be treated as if they were of the same type (parent class), allowing for more flexible code

Next steps for learning:
- Learn about polymorphism and its benefits in Python
- Explore other OOP concepts, such as encapsulation, abstraction, and multiple inheritance