Skip to main content

Inheritance

In Chapter 58, James built a Note class with __init__, summarize, add_tag, remove_tag, has_tag, and a computed word_count. Twelve tests. All green. The class validates its own title, computes its own word count, and manages its own tags.

Now he needs three kinds of notes for SmartNotes: text notes, code notes, and link notes. He does what feels natural and starts writing three separate classes:

class TextNote:
def __init__(self, title: str, body: str, author: str = "Anonymous") -> None:
if not title.strip():
raise ValueError("Title cannot be empty")
self.title = title
self.body = body
self.author = author
self.tags: list[str] = []

@property
def word_count(self) -> int:
return len(self.body.split())

def summarize(self) -> str:
tag_str = ", ".join(self.tags) if self.tags else "no tags"
return f"{self.title} ({self.word_count} words, {tag_str})"

def add_tag(self, tag: str) -> None:
if tag not in self.tags:
self.tags.append(tag)

def remove_tag(self, tag: str) -> None:
if tag not in self.tags:
raise ValueError(f"Tag '{tag}' not found")
self.tags.remove(tag)

def has_tag(self, tag: str) -> bool:
return tag in self.tags


class CodeNote:
def __init__(self, title: str, body: str, language: str,
author: str = "Anonymous") -> None:
if not title.strip():
raise ValueError("Title cannot be empty")
self.title = title
self.body = body
self.language = language
self.author = author
self.tags: list[str] = []

@property
def word_count(self) -> int:
return len(self.body.split())

def summarize(self) -> str:
tag_str = ", ".join(self.tags) if self.tags else "no tags"
return f"{self.title} ({self.word_count} words, {tag_str})"

def add_tag(self, tag: str) -> None:
if tag not in self.tags:
self.tags.append(tag)

# ... same methods again ...

Emma stops him. "Count the lines you have already copied."

James scrolls. The two classes share __init__ logic (title validation, tags initialization), word_count, add_tag, remove_tag, has_tag, and summarize. The only difference so far: CodeNote has a language field.

"What happens when you find a bug in add_tag?"

"I fix it."

"In how many places?"

James pauses. "Three. One for each note type."

"And when you add LinkNote next week?"

"Four places."

"That is the problem inheritance solves."


If you're new to programming

Inheritance means one class (the child) gets all the behavior of another class (the parent) automatically. You write shared behavior once in the parent. Each child starts with everything the parent has and can change only the parts that need to be different.

If you've coded before

Inheritance establishes an "is-a" relationship: TextNote is a Note. The child class inherits all methods and attributes from the parent. It can override specific methods and add new attributes via super().__init__(). Python uses the Method Resolution Order (MRO) to determine which version of a method runs.


The Fix: One Parent, Specialized Children

The Note class from Chapter 58 already has all the shared behavior. Instead of copying it, you tell Python that TextNote is a kind of Note:

class TextNote(Note):
pass

That is the entire class. The parentheses after TextNote name the parent. pass means "add nothing new."

Create a file called inheritance_demo.py. Put the Note class at the top (from Chapter 58), then add this TextNote below it. Run:

text = TextNote(title="Meeting Notes", body="Discussed Q3 targets", author="James")
print(text.summarize())
print(text.word_count)
text.add_tag("work")
print(text.has_tag("work"))

Output:

Meeting Notes (3 words, no tags)
3
True

Every method works. TextNote never defined summarize, add_tag, or word_count. It inherited all of them from Note.

James stares at the output. "I wrote one line -- class TextNote(Note): pass -- and it can do everything Note does?"

"Everything," Emma says. "Think about employee types at your warehouse. Every employee has a badge, a name, and a department. Warehouse workers also have a forklift certification. Office workers have a computer login. You do not write separate 'person' definitions for each. You have a base Employee and specialize."


Overriding a Method

TextNote.summarize() currently returns the same output as Note.summarize(). But a text note should show its word count prominently. Override the method by defining it again in the child:

class TextNote(Note):
def summarize(self) -> str:
tag_str = ", ".join(self.tags) if self.tags else "no tags"
return f"[TEXT] {self.title} ({self.word_count} words, {tag_str})"

Run:

text = TextNote(title="Meeting Notes", body="Discussed Q3 targets", author="James")
print(text.summarize())

Output:

[TEXT] Meeting Notes (3 words, no tags)

Python found summarize defined on TextNote and used that version instead of Note's. The parent's version still exists. It was not deleted or changed. The child's version takes priority.

Every other method (add_tag, remove_tag, has_tag, word_count) still comes from Note. You only override what needs to change.


Adding New Attributes with super().__init__()

A CodeNote needs everything Note has, plus a language field. The child's __init__ must call the parent's __init__ to reuse its validation and setup:

class CodeNote(Note):
def __init__(self, title: str, body: str, language: str,
author: str = "Anonymous") -> None:
super().__init__(title=title, body=body, author=author)
self.language = language

def summarize(self) -> str:
tag_str = ", ".join(self.tags) if self.tags else "no tags"
return f"[{self.language.upper()}] {self.title} ({self.word_count} words, {tag_str})"

super().__init__() calls Note.__init__. That call validates the title, sets self.title, self.body, self.author, and creates self.tags. After the parent finishes its setup, the child adds self.language.

Run:

code = CodeNote(title="Sort Algorithm", body="def bubble_sort(arr):", language="python")
print(code.summarize())
code.add_tag("algorithms")
print(code.has_tag("algorithms"))

Output:

[PYTHON] Sort Algorithm (3 words, no tags)
True

The parent handled title validation and tag management. The child added language tracking and a specialized summary. Neither class duplicates the other's work.

Try creating a CodeNote with an empty title:

bad = CodeNote(title="", body="print('hello')", language="python")

Output:

ValueError: Title cannot be empty

The parent's validation runs automatically through super().__init__(). You did not write validation code in CodeNote. You inherited it.


When Inheritance Makes Sense

Inheritance works when the child truly is a kind of the parent. TextNote is a Note. CodeNote is a Note. They share the same interface (title, body, tags, summarize) and differ only in specialization.

The test: can you substitute a TextNote anywhere your code expects a Note? If yes, inheritance fits. If no, you probably want a different pattern (composition, which you will learn in Lesson 3).


PRIMM-AI+ Practice: LinkNote

Predict [AI-FREE]

Press Shift+Tab to enter Plan Mode before predicting.

Here is a LinkNote subclass:

class LinkNote(Note):
def __init__(self, title: str, url: str, body: str = "",
author: str = "Anonymous") -> None:
super().__init__(title=title, body=body, author=author)
self.url = url

Answer these questions and write your confidence (1-5):

  1. Does link_note.add_tag("python") work? Why or why not?
  2. What does link_note.summarize() return? Which class's version runs?
  3. If you call LinkNote(title="", url="https://example.com"), what happens?
Check your predictions
  1. Yes. LinkNote inherits add_tag from Note. It was never overridden, so the parent's version runs. Confidence target: 4-5.
  2. Note's version. LinkNote does not define its own summarize(), so Python uses the parent's. It returns something like "Link Title (0 words, no tags)". The URL is not shown. Confidence target: 3-5.
  3. ValueError: Title cannot be empty. super().__init__() calls Note.__init__, which validates the title. Confidence target: 4-5.

Run

Press Shift+Tab to exit Plan Mode.

Create a file called link_note.py. Add the Note class at the top and the LinkNote below it. Test all three predictions by running the code. Verify that add_tag works, summarize uses the parent's version, and empty titles raise ValueError.

Investigate

In Claude Code, type:

How does Python find the add_tag method when LinkNote
does not define it? Walk me through the Method Resolution
Order for LinkNote.

Compare the AI's explanation to what you observed: Python checks LinkNote first, does not find add_tag, then checks Note, finds it, and runs it.

Modify

Override summarize() in LinkNote to include the URL:

def summarize(self) -> str:
tag_str = ", ".join(self.tags) if self.tags else "no tags"
return f"[LINK] {self.title} - {self.url} ({tag_str})"

Run it. Verify that summarize() now shows the URL while add_tag and has_tag still work unchanged.

Make [Mastery Gate]

Write a BookmarkNote(Note) subclass with:

  • A url: str attribute added via super().__init__()
  • A is_read: bool attribute defaulting to False
  • An overridden summarize() that shows [READ] or [UNREAD] before the title
  • A mark_read() method that sets is_read to True

Write 3 tests:

  1. BookmarkNote inherits add_tag from Note
  2. summarize() shows [UNREAD] for a new bookmark
  3. After mark_read(), summarize() shows [READ]

Run pyright and pytest. Both must pass.


Try With AI

Opening Claude Code

If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.

Prompt 1: When Does Inheritance Go Wrong?

Give me 3 examples where a developer used inheritance but
should not have. For each one, explain what went wrong and
what they should have done instead. Use simple examples,
not framework-specific ones.

What you're learning: Inheritance is powerful but not always correct. The AI shows you misuse patterns so you can recognize them in your own code before they cause problems.

Prompt 2: Review My Subclass

Here is my TextNote subclass:

class TextNote(Note):
def summarize(self) -> str:
tag_str = ", ".join(self.tags) if self.tags else "no tags"
return f"[TEXT] {self.title} ({self.word_count} words, {tag_str})"

Is inheritance the right choice here? Would composition
or a simple function be better? Evaluate the tradeoffs.

What you're learning: You are asking the AI to challenge your design decision, not just validate it. A good subclass passes the "is-a" test and genuinely specializes the parent. The AI's evaluation helps you see whether TextNote earns its inheritance.

Prompt 3: How Many Levels Is Too Many?

My Note class has TextNote and CodeNote as children. Could
I make RichTextNote inherit from TextNote, and
FormattedRichTextNote inherit from RichTextNote? What
problems would a 4-level deep hierarchy create?

What you're learning: Deep hierarchies make code fragile. Changing the parent can break grandchildren in unexpected ways. The AI explains why most production code stays at 1-2 levels of inheritance, which previews the composition alternative in Lesson 2.


James looks at the three subclasses: TextNote, CodeNote, LinkNote. "It is like employee categories at the warehouse," he says. "Everyone gets a badge and a department. Warehouse workers add forklift certification. Office workers add system access. The base Employee definition is written once. Each specialization adds only what is unique."

Emma nods, then pauses. "I once inherited from a class that had 15 methods. My subclass needed 2 of them. I spent more time understanding the other 13 than I would have spent writing the 2 from scratch. Inheritance solved the duplication, but it also coupled my code to 13 methods I did not care about."

"So inheritance is not always the answer?"

"It is the answer when the child is truly a specialized version of the parent. TextNote is a Note. CodeNote is a Note. That relationship is real. But what if your hierarchy gets deep? What if changing the parent breaks everything three levels down?" She writes on the whiteboard:

Inheritance solved the duplication. But what happens when the parent changes? That is the fragile base class problem. Next lesson.