Python · Syntax · Beginner

OOP: Inheritance and Dunder Methods

9 tasks

OOP problems in Python on inheritance and special methods: child classes, overriding behavior, __str__, __len__, extending base classes, and working with the object model.

Inheritance — extending existing classes

#
Inheritance lets one class reuse all the code from another class and then extend or change specific parts. The inheriting class is called a subclass or child; the class being inherited from is the parent or base class. ```python class Animal: def __init__(self, name): self.name = name class Dog(Animal): def bark(self): return f"{self.name} says woof" d = Dog("Rex") print(d.name) # Rex — inherited from Animal print(d.bark()) # Rex says woof ``` Dog inherits __init__ from Animal, so Dog("Rex") works without Dog defining its own __init__. Dog also gets any other methods Animal has. Dog only needs to define what is new or different. When the child class needs its own __init__ but still wants the parent's setup, call super().__init__(): ```python class Admin(User): def __init__(self, name, role): super().__init__(name) # runs User's __init__ self.role = role ``` super() returns a proxy that delegates to the parent class. This keeps you from duplicating the parent's initialization logic. Overriding a method means defining a method with the same name in the child class. Python always calls the most specific version — the child's method takes priority over the parent's.

Dunder methods — __str__ and __len__

#
Dunder methods (short for double underscore) are how Python lets your class respond to built-in operations. You have already seen __init__. Two more appear in these exercises. __str__ controls what Python displays when you print an object or call str() on it: ```python class Book: def __init__(self, title, pages): self.title = title self.pages = pages def __str__(self): return f"Book: {self.title}, {self.pages} pages" b = Book("Python Basics", 300) print(b) # Book: Python Basics, 300 pages ``` Without __str__, print(b) would show something like <__main__.Book object at 0x...> — the default object representation, which is rarely useful. __len__ controls what Python returns when you call len() on an object: ```python class Box: def __init__(self, items_count): self.items_count = items_count def __len__(self): return self.items_count ``` After defining __len__, len(Box(5)) returns 5. Python calls __len__ automatically — you never call it directly. The naming convention exists to avoid collisions with your own method names. __str__ and __len__ are reserved for this specific purpose, which is why they have the double underscores on both sides.

Inheritance and dunder patterns

#
Inheriting __init__ and adding new behavior — the child adds its own attributes on top of the parent's: ```python class Playlist: def __init__(self): self.songs = [] def add(self, song): self.songs.append(song) def remove(self, song): self.songs.remove(song) class CustomPlaylist(Playlist): def __init__(self, name): super().__init__() # sets up self.songs = [] self.name = name # adds the new attribute ``` super().__init__() must be called when the parent's __init__ sets up state the child depends on. Skipping it means self.songs never gets created and every call to add() raises an AttributeError. A method that uses random and inherits all other behavior: ```python from random import choice class MagicBox(Box): def random_item(self): return choice(self.items) ``` MagicBox gets everything Box has. It only needs to define the one new thing. A Message class with a flag attribute — the flag is set by the caller, the class exposes it through a method: ```python class Message: def __init__(self, text): self.text = text self.spam = False def mark_spam(self): self.spam = True ``` Attributes do not have to come from parameters. spam starts as False for every new Message — that is the right default.

Inheritance and dunder methods quick reference

#
**Inheritance syntax** ```python class Parent: def __init__(self, x): self.x = x def greet(self): return 'hello' class Child(Parent): # Child inherits from Parent def __init__(self, x, y): super().__init__(x) # run Parent's __init__ first self.y = y # then add Child's own attributes def greet(self): # override — replaces Parent's version return 'hi' ``` **`super()` — when and why** | Situation | What to do | |---|---| | Child has its own `__init__` | call `super().__init__(...)` first | | Child only adds new methods | no `__init__` needed — inherits parent's | | Overriding a method but want parent logic too | call `super().method_name()` | **Dunder methods** | Method | Called when… | Example return | |---|---|---| | `__init__(self, ...)` | `ClassName(...)` | — sets up object | | `__str__(self)` | `print(obj)` or `str(obj)` | human-readable string | | `__repr__(self)` | `repr(obj)`, debug output | unambiguous string | | `__len__(self)` | `len(obj)` | integer | | `__eq__(self, other)` | `obj == other` | `bool` | | `__lt__(self, other)` | `obj < other` | `bool` | ```python class Book: def __init__(self, title, pages): self.title = title self.pages = pages def __str__(self): return f'{self.title} ({self.pages} pages)' def __len__(self): return self.pages def __eq__(self, other): return self.title == other.title b = Book('Python', 300) print(b) # Python (300 pages) len(b) # 300 ``` **What the child inherits automatically** Everything the parent has: all methods, all attributes set in parent's `__init__`. The child only needs to define what is *new* or *different*.
01

Animal and Dog through inheritance.

#

Create an Animal class; objects created by it must have a name attribute. The value is passed to the attribute during object initialization. That is, the object describes an animal (any animal) and stores its name. Then create a Dog class that inherits from Animal . In the Dog class add a __str__ method that returns a string containing the type of animal (ahaha, sorry, "animal type" sounds funny) and its name: "Dog: Rex" # example of the returned string The idea is this: The base class Animal stores the main "behavior" of animal objects, and the Dog class specializes it for a specific type of animal.

class Animal:
    pass

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


class Dog(Animal):
    def __str__(self):
        return f"Dog: {self.name}"


# You can make it more detailed:
class Dog(Animal):
    def __str__(self):
        return f"Dog: {self.name}"

    def tell(self):
        print("Bark!!!")  # "Woof" in English. Dogs also need a translator..

class Cat(Animal):
    def __str__(self):
        return f"Cat: {self.name}"

    def tell(self):
        print("Cats don't care about human commands!")

# self still needs to be accepted even if it is not used in the method.
02

Book with string representation.

#

Create a Book class with title and pages attributes for objects. Add a __str__ method that returns: "Book: Python Basics (120 pages)" You can also add a __len__ method that returns the number of pages in the book.

class Book:
    pass
Solution
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __len__(self):
        return self.pages

    def __str__(self):
        return f"Book: {self.title} ({self.pages} pages)"
03

User and Admin.

#

Let's go through something similar to the 1st one again: Task: Create a User class with a name attribute. Create an Admin class that inherits from User . Override the __str__ method so that the output is: "Admin: Alex" And for a regular User, the output through __str__ should simply be: "Alex"

class User:
    pass

class Admin(User):
    pass
Solution
class User:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name

class Admin(User):
    def __str__(self):
        return f"Admin: {self.name}"
04

Box with items.

#

Create a Box class with an items_count attribute. Add a __len__ method that returns the number of items. Also add add and remove methods that can put objects into and take objects out of the box. This can be done simply using the items_count counter (just counting without complications), or you can make a list of items in the box (strings) and adapt the class to work with the list (the list is an object attribute).

class Box:
    pass
Solution
class Box:
    def __init__(self, items_count):
        self.items_count = items_count
    
    def add(self, count=1):
        self.items_count += count
    
    def remove(self, count=1):
        self.items_count -= count

    def __len__(self):
        return self.items_count


# With a list it is more complicated and different:
class Box:
    def __init__(self):
        self.items = []
        self.items_count = len(self.items)
    
    def add(self, item):
        self.items.append(item)
        self.items_count = len(self.items)
    
    def remove(self, item):
        if item in self.items:
            self.items.remove(item)
            self.items_count = len(self.items)
        else:
            print("The cat from the box says: There is no such thing here, goodbye!")

    def __len__(self):
        return self.items_count
05

Message with a spam flag.

#

Create a Message class with a text attribute and a spam attribute. The message text is passed into text when the object is created, while the spam attribute initially simply has the value None. Add methods: - is_spam - which checks whether the text contains any trigger words as spam criteria and returns True/False - when the is_spam method is called, besides returning True/False, the corresponding value is also stored in the self.spam attribute. - and add __len__ , which returns the length of the text.

class Message:
    pass
Solution
class Message:
    def __init__(self, text):
        self.text = text
        self.spam = None
        self.spam_triggers = ["куплю", "дорого"]  # Yes, I did not talk about this,
                                                  # but I did not forbid it either.

    def is_spam(self):
        if self.spam is not None:  # explanation below, I already "overdid" it here
            return self.spam
        
        for trigger in self.spam_triggers:
            if trigger in self.text:
                self.spam = True
                return True
            
        self.spam = False
        return False

    def __len__(self):
        return len(self.text)
    

# Having self.spam after the first call to is_spam will allow repeated checks
# to skip the checking mechanism and simply look at the ALREADY CALCULATED attribute (optimization).

m = Message("Куплю старые телевизоры дорого!!!")

if m.is_spam():  # a real check is performed
    ...

if m.is_spam():  # it will already be taken from self.spam
    ...
06

Employee and Manager.

#

Create an Employee class with name and salary attributes. Make a method in Employee that can change the salary. Create a Manager class that inherits from Employee . Override the __str__ method so that the output is: Manager John earns 5000 Make sure that the salary changing method also works for Manager

class Employee:
    pass

class Manager(Employee):
    pass
Solution
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def change_salary(self, coef): # for example coefficient 1.1 - increase by 10%
        self.salary = round(self.salary * coef) # without the second argument it rounds to an integer


class Manager(Employee):
    def __str__(self):
        return f"Manager {self.name} earns {self.salary}"
    

chel = Manager("Bob", 1000)
chel.change_salary(1.1)
print(chel)  # Manager Bob earns 1100
07

Playlist and CustomPlaylist.

#

Create a Playlist class with a songs attribute (list). Add add and remove methods for adding/removing songs. Add a reverse method that reverses the track list. Add a __len__ method that returns the number of songs. Add a __bool__ method - whether the playlist is empty or not. Then create a child class CustomPlaylist in which the add method allows adding duplicate songs to the playlist. (yes, the first one accordingly should not.)

class Playlist:
    pass
Solution
class Playlist:
    def __init__(self, songs):  # 1 argument - a list of tracks
        self.songs = songs

    def add(self, song):
        if song not in self.songs:
            self.songs.append(song)
    
    def remove(self, song):
        if song in self.songs:
            self.songs.remove(song)

    def reverse(self):
        self.songs.reverse()

    def __len__(self):
        return len(self.songs)
    
    def __bool__(self):
        return bool(self.songs)

# or

# class Playlist:
#     def __init__(self, *songs):  # tracks as separate arguments
#         self.songs = list(songs) # just decided to remind you about *args

#     # the rest is the same.


class CustomPlaylist(Playlist):
    def add(self, song):
        self.songs.append(song)
08

Named CustomPlaylist.

#

Improve the previous CustomPlaylist class by adding its own __init__, which takes the base behavior of the parent's __init__, but also creates a name for the playlist in the name attribute.

class CustomPlaylist(Playlist):
    def __init__???
        ???
    def add(self, song):
        self.songs.append(song)
Solution
class CustomPlaylist(Playlist):
    def __init__(self, songs, name):
        super().__init__(songs)
        self.name = name

    def add(self, song):
        self.songs.append(song)

    def __str__(self):
        return f"Playlist: {self.name}"
09

MagicBox with a random item.

#

Let's return to the Box class (the code is below), let's inherit from it and create a MagicBox class . The only added behavior is that when a box object is created, a random item from the list will appear inside it. Define the list of possible random items wherever you want, it does not matter, even in the global scope. Template:

from random import choice  # function for random choice from a sequence

class Box:
    def __init__(self):
        self.items = []
        self.items_count = len(self.items)
    
    def add(self, item):
        self.items.append(item)
        self.items_count = len(self.items)
    
    def remove(self, item):
        if item in self.items:
            self.items.remove(item)
            self.items_count = len(self.items)
        else:
            print("Кот из коробки говорит: Такого тут нет, досвидания!")

    def __len__(self):
        return self.items_count


things = ["Шапка", "Зонтик", "Кружка"]


class MagicBox(Box):
    def __init__(self):
        ...
Solution
from random import choice  # function for random choice from a sequence

class Box:
    def __init__(self):
        self.items = []
        self.items_count = len(self.items)
    
    def add(self, item):
        self.items.append(item)
        self.items_count = len(self.items)
    
    def remove(self, item):
        if item in self.items:
            self.items.remove(item)
            self.items_count = len(self.items)
        else:
            print("Кот из коробки говорит: Такого тут нет, досвидания!")

    def __len__(self):
        return self.items_count


things = ["Шапка", "Зонтик", "Кружка"]


class MagicBox(Box):
    def __init__(self):
        super().__init__()
        self.items.append(choice(things))
        self.items_count = len(self.items)


# OR:
class MagicBox(Box):
    def __init__(self):
        super().__init__()
        self.add(choice(things))  # this is cooler

x = MagicBox()

print(x.items)