---
title: Pillar Two: Inheritance in Python
source: https://app.sythra.ai/learn/python/inheritance-python
topic: Python
updated: 2026-08-12
publisher: Sythra (https://app.sythra.ai)
---

# Pillar Two: Inheritance in Python

Inheritance lets a child class (like Hand) automatically reuse all the methods of a parent class (like Deck) by writing class Child(Parent):, overriding only what's different — use it for genuine IS-A relationships, not just to avoid retyping code.

_Source: [https://app.sythra.ai/learn/python/inheritance-python](https://app.sythra.ai/learn/python/inheritance-python) — free to read on Sythra._

## Key points

- class Hand(Deck): gives Hand every method Deck has, for free
- Override a parent's method (like __init__) by simply redefining it in the child class
- Class attributes are shared by every instance; instance attributes belong to one object each
- Use inheritance for IS-A relationships (Hand is a Deck); use composition for HAS-A (a Car has an Engine)
- Method Resolution Order (mro()) tells you exactly which class a method call will actually use

Imagine you're writing a card game. You've already built a `Deck` class — it holds 52 cards and knows how to shuffle and deal them. Now you want to model a `Hand` — the set of cards a player holds during a game.

A hand and a deck are obviously related — both are collections of cards, both need to add and remove cards. But they're not the same thing: a hand is smaller, might score itself for poker, and belongs to a specific player. Copy-pasting the `Deck` code feels wrong — now you have two almost-identical classes, and fixing a bug in one means remembering to fix it in the other.

The right answer is **inheritance**: defining a new class that automatically gets all the methods of an existing class, and then adding or changing only what's different. The existing class is the **parent**; the new one is the **child**.

## What you will learn

- How to encode data smartly using integers instead of fragile strings
- The difference between class attributes and instance attributes
- How to make custom objects comparable with `__lt__`
- The `class Child(Parent):` syntax and overriding `__init__`
- When to use inheritance (IS-A) vs. composition (HAS-A)
- How to debug inherited methods with Method Resolution Order (MRO)

## Card objects: encoding data smartly

A playing card has a **suit** (Clubs, Diamonds, Hearts, Spades) and a **rank** (Ace through King). You could store them as strings — but strings are awkward to compare. Is `'Queen'` greater than `'Jack'`? Python compares them alphabetically, which gives unreliable answers for card games.

The better choice is **integers with a mapping**:

```text
Suits:  Clubs=0,  Diamonds=1,  Hearts=2,  Spades=3
Ranks:  Ace=1, 2=2, 3=3, ... 10=10, Jack=11, Queen=12, King=13
```

```python
class Card:
    """Represents a standard playing card."""

    def __init__(self, suit=0, rank=2):
        self.suit = suit
        self.rank = rank

# Create the Queen of Diamonds: suit=1 (Diamonds), rank=12 (Queen)
queen_of_diamonds = Card(1, 12)
```

> **Tip:** 

## Class attributes vs. instance attributes

Printing a `Card` right now gives an unhelpful memory address — you need a `__str__` that turns integer codes back into words. Where should the lookup lists live? The answer is **class attributes** — variables defined inside the class body but outside any method, belonging to the class itself, shared by every instance:

```python
class Card:
    """Represents a standard playing card."""

    # Class attributes — shared by ALL instances
    suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
    rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7',
                  '8', '9', '10', 'Jack', 'Queen', 'King']

    def __init__(self, suit=0, rank=2):
        self.suit = suit    # instance attribute
        self.rank = rank    # instance attribute

    def __str__(self):
        return f'{Card.rank_names[self.rank]} of {Card.suit_names[self.suit]}'
```

The first element of `rank_names` is `None` — a placeholder for index 0, since no card has rank 0. That keeps the index lined up: rank 12 gives `rank_names[12]`, which is `'Queen'`.

```python
card1 = Card(2, 11)
print(card1)
# → Jack of Hearts
```

> **Watch out:** 

Imagine a thousand `Card` objects in a deck — each has its own `suit` and `rank`, but `suit_names` and `rank_names`? Just one copy each, shared by all 1000 cards simultaneously.

## Comparing cards with __lt__

To sort a hand or find the highest card, Python needs to know how to compare two `Card` objects. You tell it by defining `__lt__` (less than). We'll say suit matters more than rank:

```python
# inside class Card:
def __lt__(self, other):
    return (self.suit, self.rank) < (other.suit, other.rank)
```

Python compares tuples element by element — checking suit first, moving to rank only if suits are equal. One line handles the whole comparison.

## Decks: a nested loop pattern

A deck is, at its heart, a list of cards. `__init__` builds all 52 by looping over suits and ranks:

```python
class Deck:
    def __init__(self):
        self.cards = []
        for suit in range(4):          # 0, 1, 2, 3
            for rank in range(1, 14):  # 1, 2, 3, ... 13
                card = Card(suit, rank)
                self.cards.append(card)
```

This **nested loop** — a loop inside a loop — is the standard pattern for generating all combinations of two things. For printing, building a string with repeated `+` inside a loop is slow; the efficient pattern is to collect pieces in a list, then join at the end:

```python
# inside class Deck:
def __str__(self):
    res = []
    for card in self.cards:
        res.append(str(card))
    return '\n'.join(res)
```

> **Tip:** 

A few small methods round out the `Deck`: `pop_card` (deals the last card via `list.pop()`), `add_card` (returns a card via `list.append()`), and `shuffle` (one line using `random.shuffle(self.cards)`). `pop_card` and `add_card` get a slightly dismissive nickname — **veneers** — thin wrappers over list methods that do no real computation, but make the code more readable.

## Inheritance itself: building Hand from Deck

A hand is like a deck — a collection of cards needing to add and remove — but different: it starts empty (not with 52 cards), belongs to a specific player, and might score itself for poker. With inheritance, you say: "`Hand` is a kind of `Deck`. It gets everything `Deck` has. Here's what's different." The syntax is tiny — put the parent class in parentheses:

```python
class Hand(Deck):
    """Represents a hand of playing cards."""
```

That one line gives `Hand` every method `Deck` has — `pop_card`, `add_card`, `shuffle`, `__str__` — all for free. But `Hand` needs its own `__init__`, since `Deck`'s creates 52 cards and a hand should start empty. To **override** a parent's method, just define a new one inside the child:

```python
# inside class Hand:
def __init__(self, label=''):
    self.cards = []      # empty — not 52 cards
    self.label = label   # the player's name or description
```

```python
hand = Hand('Alice')
print(hand.cards)     # []
print(hand.label)     # Alice

# But the inherited methods still work!
deck = Deck()
card = deck.pop_card()
hand.add_card(card)
print(hand)
# → King of Spades
```

We only wrote `__init__` for `Hand` — everything else came from `Deck` for free. A `move_cards` method on `Deck` makes dealing batches easy:

```python
# inside class Deck:
def move_cards(self, hand, num):
    for i in range(num):
        hand.add_card(self.pop_card())
```

Because of inheritance, `hand` can be a `Hand` object **or** a `Deck` object — `move_cards` doesn't care. It just calls `add_card` and `pop_card`, and both classes have those methods.

## IS-A vs. HAS-A: when not to use inheritance

Inheritance's downside is real: it can make programs harder to read. When you call `hand.shuffle()` and there's no `shuffle` in `Hand`, you have to go look in `Deck`. With multiple levels, you might hunt through several parents.

The rule: use inheritance only when there's a genuine **IS-A** relationship. A `Hand` IS-A kind of `Deck` — inheritance makes sense. A `Car` is NOT-A kind of `Engine` — there, make `engine` an attribute of `Car` instead (composition):

- **IS-A (inheritance)** — `Hand` is a kind of `Deck`. Use `class Hand(Deck):`.
- **HAS-A (composition)** — a `Deck` has a list of `Card`s. Use `self.cards = []` inside `Deck`.

> **Watch out:** 

## Debugging inheritance: Method Resolution Order

Inheritance introduces a new debugging puzzle: when you call a method, which class actually provides it? With a simple object you always know; with an inherited object, the method might come from the class itself, its parent, or its grandparent. The tool for this is `mro()` — **Method Resolution Order**, the ordered list of classes Python searches:

```python
def find_defining_class(obj, meth_name):
    for ty in type(obj).mro():
        if meth_name in ty.__dict__:
            return ty

hand = Hand()
print(find_defining_class(hand, 'shuffle'))
# → <class '__main__.Deck'>
```

For a `Hand`, the MRO is `[Hand, Deck, object]` — Python looks in `Hand` first, then `Deck`, then the built-in `object`.

> **Design rule:** 

## Data encapsulation: refactoring globals into a class

Sometimes programs grow organically — you start with functions and global variables, get something working, and only later realize they should be a class. This pattern is called **data encapsulation** (a narrower idea than the pillar of the same name), with a four-step recipe: write functions on global variables, get it working, look for associations between functions and variables, then wrap the related variables as attributes of a new class.

```python
# Before: global variables floating freely
suffix_map = {}
prefix = ()

# After: encapsulated inside a class
class Markov:
    def __init__(self):
        self.suffix_map = {}
        self.prefix = ()
```

The logic is unchanged — only `suffix_map` and `prefix` now live inside an object instead of floating globally, so you can run multiple analyses simultaneously with independent state. Changing a program's structure without changing its behavior is called **refactoring** — one of the most important skills in programming.

## Extra: putting inheritance to work with PokerHand

The real payoff shows up when you add specialized subclasses:

```python
class PokerHand(Hand):
    """Represents a poker hand."""

    def has_pair(self):
        ranks = [card.rank for card in self.cards]
        for rank in ranks:
            if ranks.count(rank) >= 2:
                return True
        return False

    def has_flush(self):
        suits = [card.suit for card in self.cards]
        return len(set(suits)) == 1
```

`PokerHand` inherits from `Hand`, which inherits from `Deck` — so it can `pop_card`, `add_card`, and `shuffle` without writing those methods, while adding its own poker-specific knowledge on top. Each level of the hierarchy knows what it needs and delegates the rest upward.

## Common mistakes

- Storing card ranks and suits as strings and comparing them alphabetically instead of using integers with a mapping
- Confusing a class attribute (one shared copy) with an instance attribute (one per object)
- Using inheritance for a relationship that isn't genuinely IS-A, just to avoid rewriting a few methods
- Overriding a method with a different interface than the parent, breaking code written to expect the parent's contract

> **Practice with Sythra:**  [Practice with AI tutor](https://app.sythra.ai/pricing)

## FAQ

### How do you inherit from a class in Python?

Write class Child(Parent): — the child class automatically gets every method and attribute the parent defines. You can then add new methods or override existing ones by redefining them inside the child.

### What is the difference between a class attribute and an instance attribute?

A class attribute is defined in the class body outside any method and is shared by every instance — there's only one copy. An instance attribute is set inside a method (usually __init__) via self, and each object gets its own separate copy.

### What is the difference between IS-A and HAS-A relationships?

IS-A means one class is a specialized kind of another (a Hand IS-A Deck), which calls for inheritance. HAS-A means one class contains another as an attribute (a Deck HAS-A list of Cards), which calls for composition instead.

### What does Method Resolution Order (MRO) mean in Python?

MRO is the ordered list of classes Python searches, in order, to find a method or attribute on an object. For a subclass, Python checks the subclass first, then its parent, then that parent's parent, and so on. You can inspect it with type(obj).mro().

### When should you avoid using inheritance?

Avoid inheritance when the relationship isn't a genuine IS-A — using it purely to reuse code without a real conceptual relationship tends to produce confusing, hard-to-trace bugs. Prefer composition (HAS-A) in those cases.

## Related

- [The Four Pillars of OOP](https://app.sythra.ai/learn/python/four-pillars-of-oop) — The overview these four deep-dive articles build from.
- [Pillar One: Encapsulation](https://app.sythra.ai/learn/python/encapsulation-python) — Bundling and protecting data — the previous pillar.
- [Pillar Four: Polymorphism](https://app.sythra.ai/learn/python/polymorphism-python) — How inheritance and shared interfaces enable flexible code.
- [Classes and Methods in Python](https://app.sythra.ai/learn/python/classes-and-methods-python) — self, __init__, and operator overloading — the foundation here.
- [Python course hub](https://app.sythra.ai/learn/python) — All free Python explainers and the path into Agentic practice.
- [Pillar Three: Abstraction in Python](https://app.sythra.ai/learn/python/abstraction-python) — Hiding complexity with abstract classes and @abstractmethod.

---

Written by Sythra — Learn machine learning by building. Practice this topic with Sythra's AI tutor: https://app.sythra.ai/pricing
