Python inheritance — theory with the faculty library: super(), overriding, polymorphism and abstract classes (and the ebook that lost its title)

The week we finished the faculty library catalogue (the one from the Python classes topic), someone suggested adding ebooks. “Easy,” I said: an Ebook class that inherits from Book, with its own __init__ to store the megabytes. I created it, printed the megabytes, all good. And as soon as the catalogue asked for the book’s card, this:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def card(self):
        return f'‘{self.title}’, by {self.author}'


class Ebook(Book):
    def __init__(self, title, author, megabytes):
        # missing super().__init__(title, author): title and author are never stored
        self.megabytes = megabytes


ebook = Ebook('Python for Everybody', 'Severance', 2.4)
print(ebook.megabytes)
print(ebook.card())

The real output:

2.4
Traceback (most recent call last):
  File "hook.py", line 18, in <module>
    print(ebook.card())
          ~~~~~~~~~~^^
  File "hook.py", line 7, in card
    return f'‘{self.title}’, by {self.author}'
               ^^^^^^^^^^
AttributeError: 'Ebook' object has no attribute 'title'

How can Ebook have no title if it inherits from Book and Book stores it in its __init__? That’s the trap: by writing my own __init__, the one in Book no longer runs by itself. Nobody stored the title. That slip is the most common mistake of this FP2 topic, and in this article you’ll see why it happens, how super() fixes it and everything else that comes up in the exam: method overriding, polymorphism, isinstance versus type, the object class, multiple inheritance and the method resolution order (MRO), private attributes in subclasses and abstract classes with ABC.

We keep working with the faculty library: books, magazines and three kinds of member (students, lecturers and visitors) with different loan periods and fines. The data is made up (these aren’t the real rules of the ULPGC library), but it looks like the real thing.

—

What is inheritance in Python?

Inheritance lets you create a new class from one that already exists. The class you start from is called the superclass, base class or “parent”; the new one is the subclass, derived class or “child”. The child gets all the attributes and methods of the parent without writing them again, and it can add its own or override the ones that don’t suit it.

In Python you write the parent in brackets after the child’s name:

class Book:
    """What any book in the library catalogue has."""

    def __init__(self, title, author):
        self.title = title
        self.author = author

    def card(self):
        return f'‘{self.title}’, by {self.author}'

    def loan_days(self):
        return 15                   # the library's normal loan period


class Magazine(Book):               # a Magazine IS A Book: inherits everything
    pass                            # without writing a single extra line


r = Magazine('Scientific American', 'Various authors')
print(r.card())                     # method inherited from Book
print(r.loan_days())                # inherited too
print(type(r).__name__, 'inherits from', Magazine.__bases__[0].__name__)
print('Does Magazine have its own card?', 'card' in Magazine.__dict__)
print('Does Book have its own card?    ', 'card' in Book.__dict__)

Output:

‘Scientific American’, by Various authors
15
Magazine inherits from Book
Does Magazine have its own card? False
Does Book have its own card?     True

Magazine doesn’t have a single line of its own (pass), and yet r.card() and r.loan_days() work. The last two lines prove why: card is not in the dictionary of Magazine (Magazine.__dict__); it’s in the one of Book. When you ask for r.card(), Python doesn’t find it in the object’s class and climbs to the parent. That upward search is the whole secret of inheritance: methods aren’t copied, they’re looked up.

When does inheriting make sense? When there’s an “is a” relationship between the two classes: a magazine is an item of the library, a student is a member. If the relationship is “has a” (a library has books, a member has loans), you don’t inherit: you store one object inside another, as we did with Library in the classes topic.

RelationshipExampleWhat you do
“is a”A Student is a MemberInheritance: class Student(Member)
“has a”A Library has BooksComposition: a list of books as an attribute
“is a”, but doesn’t behave the sameA DVD that can’t be taken homeInheritance + override the method that changes

In the FP2 notes you’ll see the same idea in Java with extends: class Two extends One. In Python it’s the brackets: class Two(One). And a difference that shows up in the test: Python allows multiple inheritance (several parents separated by commas); Java doesn’t.

—

Initialising subclasses: super()

Back to the hook. If the child doesn’t write __init__, it inherits the parent’s and there’s no problem. But as soon as it writes its own, that __init__ hides the parent’s: Python finds one in the child and stops climbing. That’s why, when the child needs its own __init__, the first thing it does is call the parent’s with super():

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def card(self):
        return f'‘{self.title}’, by {self.author}'


class Ebook(Book):
    def __init__(self, title, author, megabytes):
        super().__init__(title, author)    # 1) the parent stores the common part
        self.megabytes = megabytes         # 2) the child adds its own


ebook = Ebook('Python for Everybody', 'Severance', 2.4)
print(ebook.card())
print(vars(ebook))                         # all three attributes, in the same object

Output:

‘Python for Everybody’, by Severance
{'title': 'Python for Everybody', 'author': 'Severance', 'megabytes': 2.4}

Look at vars(ebook): the three attributes live in the same object. There isn’t a “Book object” inside the “Ebook object”: there’s a single object, and the parent’s __init__ gives it title and author while the child’s gives it megabytes. The diagram compares both cases:

The object with and without super().__init__Two Ebook objects. Without super().__init__ it only has megabytes and card() fails with AttributeError; with the call it has title, author and megabytes.Without super().__init__Ebook.__init__(…)# super().__init__(…)self.megabytes = megabytesebook (object)megabytes 2.4title … does not existebook.card()→ AttributeErrorWith super().__init__Ebook.__init__(…)super().__init__(title, author)self.megabytes = megabytesebook (object)title 'Python for Everybody'author 'Severance'megabytes 2.4ebook.card()→ works
What the parent stores (violet) only reaches the object if the child calls super().__init__; its own data (coral) it stores itself.

The practical rule has two parts:

  1. The child’s __init__ receives its own parameters and the parent’s (here title, author, megabytes).
  2. It passes the parent’s ones to super().__init__(…) and stores its own.

Does the order matter? It’s almost always better to call super().__init__ first, so that if the child needs something the parent sets (for example, to compute something with the title), it already exists. In the practice you’ll see a sensible exception: the Programmer validates its level before calling the parent, so nothing is created if the data is wrong.

Writing the child’s __init__ and forgetting super().__init__(…). It doesn’t fail when you create the object: the error comes later, in the first inherited method that uses a parent attribute (AttributeError: 'Ebook' object has no attribute 'title'). If you see that message with an attribute that “should” be there, look at the child’s __init__.

Why super() and not the class name

Calling the parent by its name also works:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author


class Ebook(Book):
    def __init__(self, title, author, megabytes):
        Book.__init__(self, title, author)   # works, but with self by hand
        self.megabytes = megabytes           # and the parent's name spelled out


ebook = Ebook('Python for Everybody', 'Severance', 2.4)
print(vars(ebook))

Output:

{'title': 'Python for Everybody', 'author': 'Severance', 'megabytes': 2.4}

Same result, so why do the notes insist on super()? Three reasons:

  • You don’t pass self by hand. With Book.__init__(self, …) you’re calling the class’s function and you must hand it the object yourself. With super().__init__(…), self goes automatically, as in any method.
  • You don’t repeat the parent’s name. If tomorrow Ebook inherits from Item instead of Book, with super() you change nothing; with the name, you have to hunt down every call.
  • It respects the lookup order (MRO) with multiple inheritance, which we’ll see below. super() doesn’t mean “my parent”, but “the next class in this object’s lookup order”. With a single parent they’re the same; with several, they aren’t.

—

Method overriding

The child can redefine an inherited method by writing one with the same name. It’s called overriding: when the object belongs to the child, Python finds the child’s version first and stops climbing.

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

    def limit(self):                   # how many books they can borrow at once
        return 3

    def loan_days(self):
        return 15

    def description(self):
        return f'{self.name}: {self.limit()} books, {self.loan_days()} days'


class Student(Member):
    pass                               # uses everything from Member


class Lecturer(Member):                # teaching and research staff
    def limit(self):                   # OVERRIDES Member's
        return 10

    def loan_days(self):
        return 60

    def description(self):             # EXTENDS Member's: calls it and adds
        return super().description() + ' (staff, no fines)'


for s in [Student('Laura'), Lecturer('Dr Ramos')]:
    print(s.description())

Output:

Laura: 3 books, 15 days
Dr Ramos: 10 books, 60 days (staff, no fines)

Three things happen at once here, and it’s worth separating them:

  • Student doesn’t override anything: laura.limit() climbs up to Member and returns 3.
  • Lecturer overrides limit and loan_days: its objects no longer see the ones in Member. (A Lecturer is what Spanish universities call PDI: teaching and research staff.)
  • description is defined in Member, but it calls self.limit(). When the object is a Lecturer, that self.limit() searches from the object’s class, so it finds the 10 from Lecturer. The parent calls a method the child has changed, and the child’s runs. This is what surprises people most the first time.
Member hierarchy and method lookupTree with object, Member and its children Student and Lecturer. On the right, four calls and the classes Python checks in order until it finds the method.The hierarchyobjectMemberlimit()loan_days()description()StudentLecturerlimit()loan_days()description()Arrows go from child to parentWhere does Python find the method?laura.limit()StudentMember→ 3ramos.limit()Lecturer→ 10ramos.description()Lecturersuper() → Member→ …no fines)laura.__str__()StudentMemberobject→ <…object at 0x…>
Python looks in the object's class first and climbs towards the parent until it finds the method: the first one it finds wins.

To know which method runs, don’t look at where the call is written: look at what class the object is (self) and search upwards from there. self.limit() inside Member doesn’t mean “the limit of Member“.

Calling the parent’s method from the override

Overriding doesn’t force you to throw away what the parent did. In Lecturer.description, super().description() returns the text from Member (“Dr Ramos: 10 books, 60 days”) and the child adds ” (staff, no fines)”. There are two kinds of override:

KindHow you write itWhen
Replacedef limit(self): return 10The parent’s behaviour is of no use
Extendreturn super().description() + ' (staff…)'The parent’s part is fine and the child adds something before or after

Extending and forgetting the return: text = super().description() + '…' and nothing else. The method returns None and the print shows None. You’ll see it with real output in the “What’s wrong here?” section of the exercises.

In Java an override is marked with @Override and the compiler warns you if the signature doesn’t match. Python has no such safety net: if in the child you write def limits(self) (one s too many), you don’t override anything, you create a new method and the parent keeps using its own. Abstract classes, at the end of the article, partly protect you from that.

—

Polymorphism: same interface, different behaviour

Polymorphism means the same code works with objects of different classes, and each one responds in its own way. The fine function below computes any member’s fine without asking what kind of member it is: it asks for the loan period and the fine per day, and each object answers its own.

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

    def loan_days(self):
        return 15

    def fine_per_day(self):               # euros per day late
        return 0.20


class Lecturer(Member):
    def loan_days(self):
        return 60

    def fine_per_day(self):
        return 0.0


class Visitor(Member):                    # one-day card
    def loan_days(self):
        return 7

    def fine_per_day(self):
        return 0.50


def fine(member, days_out):
    """Doesn't know (or care) what class the member is: it asks."""
    delay = max(0, days_out - member.loan_days())
    return delay * member.fine_per_day()


for s in [Member('Laura'), Lecturer('Dr Ramos'), Visitor('Pablo')]:
    print(f'{type(s).__name__:<9} {s.name:<10} 20 days out -> {fine(s, 20):.2f} €')

Output:

Member    Laura      20 days out -> 1.00 €
Lecturer  Dr Ramos   20 days out -> 0.00 €
Visitor   Pablo      20 days out -> 6.50 €

Check the numbers by hand: 20 days out with a 15-day period is 5 days late; 5 × 0.20 = 1.00 € for Laura. For the lecturer, 20 − 60 is negative and max(0, …) turns it into 0. For the visitor, 20 − 7 = 13 days × 0.50 = 6.50 €.

What matters is what’s not in fine: not a single if type(member) == …. If tomorrow the library creates AlumniMember with 30 days and 0.10 €/day, the function works without touching it. That’s the practical advantage of polymorphism: code that uses the objects doesn’t change when new types appear.

In Python, polymorphism doesn’t even need inheritance. All fine needs is an object with loan_days() and fine_per_day(); if a class unrelated to Member has them, it works too. That’s duck typing (“if it walks like a duck and quacks…”). Inheritance is there to avoid repeating code and to write the contract down; polymorphism comes from calling the same methods.

—

isinstance vs type: the difference that confuses most

Two functions look like they do the same thing and they don’t:

  • type(obj) returns the object’s exact class.
  • isinstance(obj, Class) tells you whether the object belongs to that class or any of its children.
class Member:
    def loan_days(self):
        return 15


class Student(Member):
    pass


class Lecturer(Member):
    def loan_days(self):
        return 60


e = Student()
print(type(e) == Student, type(e) == Member)                 # EXACT type
print(isinstance(e, Student), isinstance(e, Member))         # respects inheritance
print(isinstance(e, object), issubclass(Lecturer, Member))
print([c.__name__ for c in Student.__mro__])                 # lookup order
print('__str__' in dir(e), '__str__' in Member.__dict__)     # object provides it

Output:

True False
True True
True True
['Student', 'Member', 'object']
True False

Line by line: type(e) == Member is False because e is exactly an Student; isinstance(e, Member) is True because a student is a member. issubclass asks the same question with classes instead of objects. And __mro__ (Method Resolution Order) is the exact list of classes Python goes through when looking up a method: Student → Member → object.

I want to know…UseWith e = Student()
Whether I can treat it as a Memberisinstance(e, Member)True
Its exact classtype(e)Student
Whether a class inherits from anotherissubclass(Student, Member)True
In what order methods are looked upStudent.__mro__Student, Member, object

The rule for the exam and for life: isinstance almost always. Comparing with type breaks inheritance: if you filter “the members” with type(s) == Member, you lose every student and lecturer. In the practice you’ll see exactly that bug with the petrol cars and the hybrid.

Using isinstance to decide what to do with each type (if isinstance(s, Lecturer): … elif isinstance(s, Visitor): …). It works, but it’s the opposite of polymorphism: every new type forces you to edit that if. If you catch yourself writing that chain, that behaviour should be a method each class overrides.

—

The object class: the root of everything

The last line of the previous output has a twist: '__str__' in dir(e) is True, but '__str__' in Member.__dict__ is False. Where does that __str__ come from? From object: in Python 3 every class inherits from object even if you don’t write it (class Member: is the same as class Member(object):). That’s why any object has __str__, __repr__, __eq__… even if you didn’t define them, and why print(laura) without __str__ prints the famous <__main__.Student object at 0x…>: it’s the __str__ of object.

When you wrote __str__ or __repr__ in the classes topic, you were overriding the ones in object. And in the next topic, operator overloading, you’ll do the same with __eq__, __lt__ or __add__.

—

Multiple inheritance and the MRO

Python lets a class have several parents. A research fellow teaches (like a lecturer) and studies a master’s degree at the same time (like a student). What’s their loan limit?

class Student:
    def limit(self):
        return 3

    def loan_days(self):
        return 15


class Lecturer:
    def limit(self):
        return 10

    def loan_days(self):
        return 60


class Fellow(Lecturer, Student):    # teaches AND studies a master's
    def loan_days(self):
        return 30                   # its own


b = Fellow()
print([c.__name__ for c in Fellow.__mro__])
print('limit:', b.limit())          # doesn't have it: Lecturer wins (comes first)
print('days:', b.loan_days())       # it has its own: Fellow wins

Output:

['Fellow', 'Lecturer', 'Student', 'object']
limit: 10
days: 30

Python decides with the MRO: it looks in Fellow, then in the parents in the order you wrote them (Lecturer, then Student) and finally in object. limit isn’t in Fellow, so Lecturer‘s wins (10). loan_days is in Fellow, so its own wins (30). If you had written class Fellow(Student, Lecturer), the limit would be 3.

Multiple inheritance has good uses (small classes that add one ability, called mixins), but in FP2 what you’re asked is to understand the MRO and predict which method wins. If in doubt, print Class.__mro__: it’s the exact answer.

—

Subclass attributes

The child sees everything the parent stores in the object… with one exception that confuses a lot: attributes with two underscores.

class Member:
    def __init__(self, name, code):
        self.name = name            # public
        self._card = code[:3]       # "protected": a convention, no renaming
        self.__code = code          # private: Python stores it as _Member__code


class Student(Member):
    def summary(self):
        return f'{self.name} ({self._card}) {self.__code}'


e = Student('Laura', 'EST-4471')
print([k for k in vars(e)])
print(e.summary())

Output:

['name', '_card', '_Member__code']
Traceback (most recent call last):
  File "private.py", line 15, in <module>
    print(e.summary())
          ~~~~~~~~~^^
  File "private.py", line 10, in summary
    return f'{self.name} ({self._card}) {self.__code}'
                                         ^^^^^^^^^^^
AttributeError: 'Student' object has no attribute '_Student__code'

vars(e) explains it: the __code attribute written by Member was stored as _Member__code (name mangling, which we saw in the classes topic). Inside Student, writing self.__code turns into self._Student__code, which doesn’t exist. That’s why the traceback names _Student__code.

What it’s calledExampleCan the child use it with that name?
Publicself.nameYes
“Protected” (one underscore)self._cardYes: it’s a convention, “internal use, children included”
Private (two underscores)self.__codeNo: it’s called _Member__code; use a property or a parent method

The clean way out: if children need some data, the parent offers it through a property (@property) or stores it with a single underscore. What you shouldn’t do is write self._Member__code in the child: it works, but it bypasses the encapsulation you wanted.

—

Abstract classes: contracts that must be fulfilled

In the library there’s no such thing as “an item” on its own: there are books, magazines, DVDs. Item is an idea that gathers what they share, but creating a Item('What am I?') makes no sense. That’s what abstract classes are for: a class that inherits from ABC (Abstract Base Class, from the abc module) and marks with @abstractmethod the methods every child must write.

from abc import ABC, abstractmethod


class Item(ABC):                        # ABC: Abstract Base Class
    def __init__(self, title):
        self.title = title

    @property
    @abstractmethod
    def kind(self):                     # abstract property (as in the notes)
        pass

    @abstractmethod
    def loan_days(self):
        pass

    def card(self):                     # normal method that USES the abstract ones
        return f'[{self.kind}] {self.title}: {self.loan_days()} days'


class Book(Item):
    @property
    def kind(self):
        return 'Book'

    def loan_days(self):
        return 15


class Magazine(Item):
    @property
    def kind(self):
        return 'Magazine'

    def loan_days(self):
        return 7


for m in [Book('Calculus'), Magazine('Scientific American')]:
    print(m.card())

try:
    Item('What am I?')
except TypeError as err:
    print('TypeError:', err)

Output:

[Book] Calculus: 15 days
[Magazine] Scientific American: 7 days
TypeError: Can't instantiate abstract class Item without an implementation for abstract methods 'kind', 'loan_days'

Three things to look at:

  1. kind is an abstract property: @property above @abstractmethod, in that order (it’s what the notes show with area). Children implement it as a property too, which is why card writes self.kind without brackets.
  2. card is a normal method inside the abstract class, and it uses the abstract ones. The parent writes the recipe and the children provide the ingredients. This pattern is called the template method, and you’ll see it in all three programs of the practice.
  3. Creating an Item raises TypeError, and the message says exactly what’s missing: without an implementation for abstract methods 'kind', 'loan_days'.

And if a child fulfils only half of the contract?

from abc import ABC, abstractmethod


class Item(ABC):
    def __init__(self, title):
        self.title = title

    @abstractmethod
    def loan_days(self):
        pass

    @abstractmethod
    def kind(self):
        pass


class Dvd(Item):
    def loan_days(self):             # fulfils only half of the contract
        return 3


d = Dvd('EII documentary')

Output:

Traceback (most recent call last):
  File "incomplete.py", line 22, in <module>
    d = Dvd('EII documentary')
TypeError: Can't instantiate abstract class Dvd without an implementation for abstract method 'kind'

Dvd wrote loan_days but not kind, so it’s abstract too and can’t be instantiated either. The good part is when the error appears: when creating the object, not days later when someone calls kind(). It’s the safety net Python lacks compared to Java’s @Override.

An abstract class is a contractItem inherits from ABC and declares kind and loan_days as abstract. Book and Magazine implement both and can be created; Dvd only implements one and raises TypeError, like Item itself.Item(ABC)@property @abstractmethod kind@abstractmethod loan_days()card() # uses bothBookkind -> 'Book'loan_days() -> 15Book(…) worksMagazinekind -> 'Magazine'loan_days() -> 7Magazine(…) worksDvdloan_days() -> 3kind -> missingTypeErrorItem('What am I?') → TypeError
Only classes that fulfil the whole contract can create objects: Dvd lacks kind and cannot be instantiated.

Inheriting from ABC but forgetting @abstractmethod. Then the “abstract” method with pass is a normal method that returns None, the class can be instantiated and the bug shows up late again. Both go together: class X(ABC) and @abstractmethod on every required method.

If a test question says “what happens when you run Shape() if Shape has an unimplemented abstract method?”, the answer is TypeError when the object is created, not when the method is called.

—

Abstract class vs normal class: when to use each

SituationChooseLibrary example
The parent makes sense on its own and the children are variantsNormal classMember with default values; Lecturer changes the period
The parent is just a shared idea; there are no “generic” objectsAbstract class (ABC)Item: it’s always a book, a magazine or a DVD
You want a forgotten method to fail when the object is createdAbstract class with @abstractmethodloan_days in every item
There’s a sensible default value that some children changeNormal method in the parent (hook)fine_per_day() returns 0.20 and the lecturer overrides it with 0

The last row is the most useful in practice: not everything has to be abstract. A normal method with a default value that some children override (a hook) saves you writing the same thing in five classes.

—

Common mistakes with inheritance

Forgetting super().__init__ in a child with its own __init__: AttributeError in the first inherited method that uses a parent attribute.

Changing the signature when overriding a method (def loan_days(self, in_summer) in the child when the parent calls it with no arguments): TypeError: … missing 1 required positional argument. Children must accept the same as the parent.

Using the parent’s self.__attribute in the child: AttributeError with _Child__attribute. Use a property or a single underscore.

Filtering with type(x) == Parent: children slip through. Use isinstance.

Misspelling the name of the method you override (loans_days): you override nothing. With an abstract class the error appears when creating the object; without one, the parent silently keeps using its own.

—

Watch it step by step in Python Tutor

pythontutor.com runs the code line by line and draws the objects, their attributes and the call stack. Choose Python 3, paste this program and press Visualize Execution:

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

    def days(self):
        return 15

    def card(self):
        return self.name + ': ' + str(self.days()) + ' days'


class Lecturer(Member):
    def __init__(self, name, dept):
        super().__init__(name)
        self.dept = dept

    def days(self):
        return 60


members = [Member('Laura'), Lecturer('Ramos', 'Computing')]
for s in members:
    print(s.card())

Output:

Laura: 15 days
Ramos: 60 days

What to watch step by step:

  1. When Lecturer('Ramos', 'Computing') is created, look at the stack on the right: two __init__ frames appear at once, the one of Lecturer and, on top, the one of Member called by super(). Both have the same self (the arrow points to the same object).
  2. When they finish, the object has name and dept: the parent set one and the child the other, but it’s a single object.
  3. In the second iteration of the for, it enters card (which lives in Member) and from there calls self.days(). Look where it jumps: to Lecturer‘s version, because self is a Lecturer.
  4. In the first iteration, the same self.days() jumps to Member‘s. Same line, two destinations: that’s polymorphism live.

—

Quick summary

ConceptHow you write itWhat to remember
Inheritclass Child(Parent):Methods aren’t copied: they’re looked up upwards
Initialisesuper().__init__(…)Required if the child writes its own __init__
Overridesame name in the childSame signature as the parent
Extendreturn super().method() + …Don’t forget the return
Polymorphismsame calls, different objectsNo chains of if type(…)
Check the typeisinstance(obj, Class)type ignores inheritance
Lookup orderClass.__mro__With multiple inheritance, left to right
Private__x → _Parent__xThe child doesn’t see it with that name
Abstractclass X(ABC) + @abstractmethodTypeError on creation if something is missing

Three ideas to keep:

  1. Python looks methods up from the object’s class upwards, and the first one it finds wins. Almost everything in this topic follows from that.
  2. If the child writes __init__, it calls super().__init__: otherwise the parent’s data never reaches the object.
  3. An abstract class turns a slip into an early error: better a TypeError when creating the object than a weird None three days later.

In the inheritance practice we build three complete hierarchies (a payroll, a car fleet with a hybrid that inherits from a petrol car, and a shop with IGIC) and you’ll see the template method in action.

Next step: practicePython inheritance practice — a payroll, a fleet with a hybrid and a shop with IGIC (and the formula the hybrid copied instead of inheriting)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *