# Polymorphism: a Common Interface, Different Behavior<no value>

## What will we learn?

- What **Polymorphism** is.
- **A Common Interface**: the same method, different behavior for each type.
- Why it **replaces** the `if type == ...`.
- **Duck Typing**: Python's flexible version.

## Why do we need this?

Suppose a payment system needs to charge a customer, and the customer can pay by credit card, by PayPal or by bank transfer. Without Polymorphism:

```python
def pay(method, amount):
    if method["type"] == "credit":
        print("Charging card", method["card"], amount)
    elif method["type"] == "paypal":
        print("Redirecting", method["email"], "to PayPal for", amount)
    elif method["type"] == "bank":
        print("Waiting for transfer of", amount, "to account", method["account"])
```

And what happens when you add Apple Pay? You have to add an `elif` **here**, and also in the refund function, and also in the monthly report, and also in the fee check... Every function that handles payments contains the same `if` chain. **You forget one, and there is a bug.**

In the first OOP lesson this was "problem 4". Polymorphism is its solution.

## Explanation from the basics

### What is Polymorphism?

**Polymorphism** (from Greek: "many forms") = **the ability to work with objects of different types through the same interface**, where every object performs the action **in its own way**.

In simple words: **the same call** (`method.pay(100)`) → **different behavior**, according to the object's type. The calling code **does not need to know** which type it is holding.

{{< box "analogy" "Analogy: a universal remote control" >}}
The remote has an "on" button. You press it in front of a TV, and it turns on. In front of an air conditioner, it starts cooling. In front of a speaker, it starts playing. **The same button** (an interface), **a different action** for every device. And you, holding the remote, do not need to know how each device works inside. A new device arrives? If it "understands" the button, the remote works with it without any change.
{{< /box >}}

### How it works: a common interface

1. In the **Parent**, define **the interface**: which methods all the types have (`pay`, `refund`, `fee`).
2. Every **Child** **overrides** the methods in its own way.
3. The code that uses them works against **the interface**: `method.pay(amount)`, and not against the types.

**We have already seen this!** In the `describe()` of Employee, which called `self.monthly_cost()` and reached the right version of a manager or an intern.

### The main advantage: Open/Closed

With Polymorphism, adding a new type (Apple Pay) = **writing one new Class**. **Not a single line** in the existing code that uses payments changes. The system is **open to extension and closed to modification**, an important principle in software design.

### Duck Typing

In Python, Polymorphism does not **require** inheritance. Python does not check "does the object inherit from PaymentMethod?". It only tries to call the method. If the method exists, it works.

> "If it walks like a duck and quacks like a duck, it's a duck."

**Duck Typing** = "it does not matter what **you are**, it matters what you **know how to do**". We have already used this: `len()` works on `str`, `list`, `dict` and `set`, because they all have a "length". `for` works on anything you can go over.

And still, **inheriting from a common Parent** is the recommended way when you design an interface on purpose: it **documents** what the interface is, and allows shared code.

## Syntax

```python
class Base:
    def action(self):                    # the common interface
        raise NotImplementedError("subclasses must implement action()")

class TypeA(Base):
    def action(self):
        return "A does it this way"

class TypeB(Base):
    def action(self):
        return "B does it another way"

for item in [TypeA(), TypeB()]:          # polymorphic code:
    print(item.action())                 # one call, many behaviors, no if/elif
```

**`raise NotImplementedError`** in the Parent = "every Child **must** implement this method". If a Child forgot, you get a clear error at the moment of the call.

## First code example

**The payment system, with Polymorphism.** `week-3/payments.py`:

```python
class PaymentMethod:
    def __init__(self, owner):
        self.owner = owner

    def pay(self, amount):
        raise NotImplementedError("subclasses must implement pay()")

    def fee(self, amount):
        return 0

class CreditCard(PaymentMethod):
    def __init__(self, owner, last_digits):
        super().__init__(owner)
        self.last_digits = last_digits

    def pay(self, amount):
        return "Charged " + str(amount) + " to card ****" + self.last_digits

    def fee(self, amount):
        return round(amount * 0.02, 2)

class PayPal(PaymentMethod):
    def __init__(self, owner, email):
        super().__init__(owner)
        self.email = email

    def pay(self, amount):
        return "Sent " + str(amount) + " via PayPal from " + self.email

class BankTransfer(PaymentMethod):
    def pay(self, amount):
        return "Waiting for a bank transfer of " + str(amount) + " from " + self.owner

def checkout(method, amount):
    total = amount + method.fee(amount)
    print(method.pay(total), "| fee:", method.fee(amount))

methods = [CreditCard("Dana", "4321"), PayPal("Avi", "avi@mail.com"), BankTransfer("Noa")]
for method in methods:
    checkout(method, 100)
```

**Output:**

```text
Charged 102.0 to card ****4321 | fee: 2.0
Sent 100 via PayPal from avi@mail.com | fee: 0
Waiting for a bank transfer of 100 from Noa | fee: 0
```

## Breaking down the code line by line

| Code | Explanation |
|---|---|
| `class PaymentMethod` | **The interface**: every payment method **must** have `pay`, and has `fee` (default 0). |
| `raise NotImplementedError` | An "abstract method": it defines an interface, without an implementation. |
| `CreditCard.pay`, `PayPal.pay`, `BankTransfer.pay` | Each one implements `pay` **in its own way**. |
| `CreditCard.fee` | An Override: a 2% fee. The others **inherit** the Parent's 0. |
| `def checkout(method, amount):` | **The polymorphic code**: it does not check the type, it contains no `if`. It only calls `method.fee()` and `method.pay()`. |
| The loop | Exactly the same function, three different behaviors. |

## Another example

**Adding a new type — without touching existing code.** And then Duck Typing.

```python
class ApplePay(PaymentMethod):
    def __init__(self, owner, device):
        super().__init__(owner)
        self.device = device

    def pay(self, amount):
        return "Paid " + str(amount) + " with Apple Pay on " + self.device

    def fee(self, amount):
        return 1

checkout(ApplePay("Tom", "iPhone 15"), 100)
```

```text
Paid 101 with Apple Pay on iPhone 15 | fee: 1
```

**`checkout` did not change at all.** We added a Class, and it works. In the `if`/`elif` version we would have had to edit every function that handles payments.

**Duck Typing — an object that does not inherit from PaymentMethod:**

```python
class GiftCard:
    def __init__(self, code):
        self.code = code

    def pay(self, amount):
        return "Redeemed " + str(amount) + " from gift card " + self.code

    def fee(self, amount):
        return 0

checkout(GiftCard("GIFT-2026"), 100)
```

```text
Redeemed 100 from gift card GIFT-2026 | fee: 0
```

`GiftCard` does **not** inherit from `PaymentMethod`, but it has `pay` and `fee`, so `checkout` works with it. This is Duck Typing. (In a real project it is still better for it to inherit, so that the interface is documented and explicit.)

### Another familiar example: notifications

```python
class Notification:
    def __init__(self, recipient):
        self.recipient = recipient

    def send(self, text):
        raise NotImplementedError

class EmailNotification(Notification):
    def send(self, text):
        return "Email to " + self.recipient + ": " + text

class SmsNotification(Notification):
    def send(self, text):
        return "SMS to " + self.recipient + ": " + text[:20]

def notify_all(channels, text):
    for channel in channels:
        print(channel.send(text))

notify_all([EmailNotification("dana@mail.com"), SmsNotification("050-1234567")],
           "Your order #1042 has shipped and will arrive tomorrow")
```

```text
Email to dana@mail.com: Your order #1042 has shipped and will arrive tomorrow
SMS to 050-1234567: Your order #1042 has
```

Every channel has its own rules (SMS is cut to 20 characters), and `notify_all` does not know and does not care.

## Common mistake

**Mistake 1: checking the type instead of using Polymorphism.**

```python
def checkout(method, amount):
    if isinstance(method, CreditCard):
        ...
    elif isinstance(method, PayPal):
        ...
```

This takes us back exactly to the original problem, only with Classes. If you find yourself writing `if isinstance(...)` on **your own** Classes to decide **how to perform an action**, it is a sign that the action should be a **method** that every Class implements.

**Mistake 2: forgetting to implement an abstract method.**

```python
class Cash(PaymentMethod):
    pass

checkout(Cash("Rina"), 50)
```

```text
NotImplementedError: subclasses must implement pay()
```

And that is **good**: the error is clear and points exactly at what is missing. Without the `raise` in the Parent, you would get a silent, wrong behavior, or `None` instead of a string.

**Mistake 3: an inconsistent interface between the types.**

```python
class CreditCard(PaymentMethod):
    def pay(self, amount, cvv):
        ...
```

Now `checkout` (which calls `method.pay(total)`) fails only on credit cards. **All the implementations must receive the same parameters.** Information specific to a type (like a CVV) belongs in the `__init__` or in an Attribute of that type.

**Mistake 4: thinking that Polymorphism requires inheritance.** In Python it does not (Duck Typing). But inheriting from a common Base **documents** the interface, and prevents mistakes. Prefer it when you design.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

**What you practice on this page:** polymorphism — the same call for several types, where each type behaves differently; adding a new type without changing existing code; Duck Typing and `NotImplementedError`.

**By the end of the page you will know how to:**
- Replace an `if` chain by type with a shared method that every type implements
- Add a new type without touching the code that uses it
- Explain what Duck Typing is
- Force Children to implement a method using `NotImplementedError`

### Exercise 1: Instead of an if chain

**What you build:** payment methods at the checkout — without checking in the code what the type of each one is.

**The code you replace:**

```text
def pay(method, amount):
    if method["type"] == "credit":
        return "Charging card " + str(amount)
    elif method["type"] == "paypal":
        return "Redirecting to PayPal " + str(amount)
```

**What to write:**
1. A base Class named `PaymentMethod`, with `pay` that receives an amount and throws `NotImplementedError`.
2. Two Children: `CreditCard` and `PayPal`. Each one overrides `pay` and returns its own sentence from the output.
3. A function named `checkout` that receives a payment method and an amount, and calls `pay` — without an `if` on the type.

**Test code:**

```python
for method in [CreditCard(), PayPal()]:
    print(checkout(method, 100))
```

**The desired output:**

```text
Charging card 100
Redirecting to PayPal 100
```

{{< solution >}}
```python
class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError("subclasses must implement pay()")


class CreditCard(PaymentMethod):
    def pay(self, amount):
        return "Charging card " + str(amount)


class PayPal(PaymentMethod):
    def pay(self, amount):
        return "Redirecting to PayPal " + str(amount)


def checkout(method, amount):
    return method.pay(amount)


for method in [CreditCard(), PayPal()]:
    print(checkout(method, 100))
```

```text
Charging card 100
Redirecting to PayPal 100
```

`checkout` does not know and does not need to know which kind of payment it is — it only calls `pay`. Each type implements it differently. That is **Polymorphism**.
{{< /solution >}}

### Exercise 2: A new type — without touching checkout

**What you build:** continuing the checkout — adding a new payment method and a fee.

**What to write:**
1. A new Child named `ApplePay`, with `pay` that returns its own sentence from the output.
2. In `PaymentMethod`, add `fee` that receives an amount and returns 0 — a default for everyone.
3. In `CreditCard`, override `fee`: a fee of 2% of the amount.
4. Update `checkout` so that it also returns the fee, as in the output.

**Test code:**

```python
for method in [CreditCard(), PayPal(), ApplePay()]:
    print(checkout(method, 100))
```

**The desired output:**

```text
Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0
```

{{< solution >}}
```python
class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError("subclasses must implement pay()")

    def fee(self, amount):
        return 0


class CreditCard(PaymentMethod):
    FEE_PERCENT = 2

    def pay(self, amount):
        return "Charging card " + str(amount)

    def fee(self, amount):
        return amount * CreditCard.FEE_PERCENT / 100


class PayPal(PaymentMethod):
    def pay(self, amount):
        return "Redirecting to PayPal " + str(amount)


class ApplePay(PaymentMethod):
    def pay(self, amount):
        return "Paid with Apple Pay " + str(amount)


def checkout(method, amount):
    return method.pay(amount) + " | fee: " + str(method.fee(amount))


for method in [CreditCard(), PayPal(), ApplePay()]:
    print(checkout(method, 100))
```

```text
Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0
```

`ApplePay` was added without changing a single line in `checkout`. `PayPal` and `ApplePay` inherit the Parent's `fee`, and `CreditCard` overrides it.
{{< /solution >}}

### Exercise 3: Characters in a game

**What you build:** a fight between characters in a game — where every kind of character has a different attack power.

**What to write:**
1. A base Class named `Character`, with a name and health, and `attack_power` that throws `NotImplementedError`.
2. `Warrior` — its attack power is always 15.
3. `Mage` — its attack power is 10, plus 10 if it has more than 50 health.
4. A function named `attack` that receives an attacker and a target: it takes health away from the target according to the attacker's power, and prints a sentence as in the output.

**Test code:**

```python
conan = Warrior("Conan", 100)
merlin = Mage("Merlin", 60)
attack(conan, merlin)
attack(merlin, conan)
print(merlin.name, merlin.health)
print(conan.name, conan.health)
```

**The desired output:**

```text
Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90
```

{{< solution >}}
```python
class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health

    def attack_power(self):
        raise NotImplementedError("subclasses must implement attack_power()")


class Warrior(Character):
    def attack_power(self):
        return 15


class Mage(Character):
    STRONG_FROM = 50

    def attack_power(self):
        if self.health > Mage.STRONG_FROM:
            return 20
        return 10


def attack(attacker, target):
    power = attacker.attack_power()
    target.health = target.health - power
    print(attacker.name, "hits", target.name, "for", power)


conan = Warrior("Conan", 100)
merlin = Mage("Merlin", 60)
attack(conan, merlin)
attack(merlin, conan)
print(merlin.name, merlin.health)
print(conan.name, conan.health)
```

```text
Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90
```

After Conan attacked, Merlin was left with 45 health — below 50 — so his power dropped to 10. `attack` knows nothing about each character's rules.
{{< /solution >}}

### Exercise 4: Duck Typing

**What you do:** read code and explain — without writing code.

**The code:**

```text
class Duck:
    def speak(self):
        return "Quack"

class Robot:
    def speak(self):
        return "Beep"

def make_it_speak(thing):
    return thing.speak()
```

**The task:**
1. `Duck` and `Robot` do not inherit from the same Parent. Will `make_it_speak` work with both? Why?
2. Explain in one sentence what Duck Typing is.

{{< solution >}}
1. **Yes.** `make_it_speak` only calls `speak()` — it does not check the type. Any object that has `speak` fits.
2. **Duck Typing**: "if it walks like a duck and quacks like a duck — it is a duck". In Python what matters is **that the method exists**, not which Class the object comes from. A common base Class is still useful — to define the interface in one place and give a default behavior — but it is not required.
{{< /solution >}}

### Exercise 5: NotImplementedError

**What you build:** shapes that each know how to calculate their own area — and a Parent that requires it.

**The code:**

```text
class Shape:
    def area(self):
        raise NotImplementedError("subclasses must implement area()")

class Square(Shape):
    def __init__(self, side):
        self.side = side
```

**The task:**
1. What will happen when you call the area of `Square(3)`? Why is that actually good?
2. Fix `Square` so that it calculates an area.
3. Add `Circle` that receives a radius, and calculates the area with `math.pi`, rounded to two digits.
4. Print in a loop the area of a square with side 3 and a circle with radius 3.

**The desired output:**

```text
9
28.27
```

{{< solution >}}
1. A `NotImplementedError: subclasses must implement area()` will be thrown. That is **good**: the mistake is discovered immediately and clearly — whoever wrote `Square` forgot to implement `area`. Without it, a wrong result would come out silently.
2.

```python
import math


class Shape:
    def area(self):
        raise NotImplementedError("subclasses must implement area()")


class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return round(math.pi * self.radius ** 2, 2)


for shape in [Square(3), Circle(3)]:
    print(shape.area())
```

```text
9
28.27
```
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Polymorphism** = the same call, different behavior according to the object's type. The calling code does not need to know the type.
- Build a **common interface** in the Parent (with `raise NotImplementedError` for required methods), and every Child implements it in its own way.
- It **replaces** chains of `if isinstance` / `if type ==`. A new type = a new Class, without changing existing code.
- A Parent's method that calls `self.x()` reaches the Child's version (Template Method).
- **Duck Typing**: in Python it is enough that the object has the method. A common inheritance is still recommended as documentation.
- All the implementations receive **the same parameters**.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [Python Glossary — duck-typing](https://docs.python.org/3/glossary.html#term-duck-typing)
- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)

<!-- docs:end -->
