PythonIntermediate 13 min Lesson 18 of 30

Day 18 — Polymorphism and Encapsulation

One call, many behaviours: how duck typing works in Python, and how to hide internal state so objects stay valid as your code grows.

Python · Lesson 18 of 30
0/30 done(0%)

What is it? #

Polymorphism means the same call does the right thing for different types. You call .send() and each notification type sends itself its own way. The calling code does not branch on type.

Python gets there through duck typing: if an object has the method you call, it works. There is no requirement to share a base class. "If it walks like a duck and quacks like a duck, treat it as a duck."

Encapsulation means hiding internal details behind a small public surface. Callers use methods and properties; they do not reach in and rearrange attributes.

Python has no real private keyword. It uses convention: a single leading underscore means internal, and a double underscore triggers name mangling that makes accidental access harder. The discipline comes from the team, not the compiler.

Think of it like this #

Polymorphism is a power socket. Any appliance with the right plug works — the socket does not care whether it is a lamp or a laptop charger.

Encapsulation is the appliance casing. There are wires inside, but you are given a switch and a plug. You can still unscrew the case, and sometimes you must, but you know you are doing something unusual.

Simple example #

An exporter needs to write a report in several formats. Instead of an if-else chain over format names, each exporter class exposes the same method. Separately, a temperature sensor object protects its readings so nobody can set an impossible value.

Code #

PYTHON
class CsvExporter:
    def export(self, rows):
        return "\n".join(",".join(str(v) for v in row) for row in rows)

class JsonExporter:
    def export(self, rows):
        import json
        return json.dumps(rows)

class TextExporter:
    def export(self, rows):
        return "\n".join(" | ".join(str(v) for v in row) for row in rows)


def write_report(exporter, rows):
    # No if/elif on the format — any object with .export() works
    return exporter.export(rows)


rows = [["sku", "qty"], ["A-1", 3]]
for exporter in (CsvExporter(), JsonExporter(), TextExporter()):
    print(write_report(exporter, rows))


class Thermostat:
    def __init__(self, target=22):
        self.__target = None          # name-mangled to _Thermostat__target
        self.target = target          # goes through the setter below

    @property
    def target(self):
        return self.__target

    @target.setter
    def target(self, value):
        if not 5 <= value <= 35:
            raise ValueError("target must be between 5 and 35")
        self.__target = value


t = Thermostat()
t.target = 26
print(t.target)                        # 26
# t.target = 99                        # ValueError

How it works #

The three exporter classes share no base class. They just each define export. write_report calls exporter.export(rows) and Python resolves it at runtime on whatever object it was given. Adding a fourth format means adding a class, not editing write_report — which is the Open/Closed Principle in action.

The loop passes three different types into the same function, and each does its own thing. That is polymorphism with no inheritance involved at all.

In Thermostat, __target with two leading underscores gets renamed internally to _Thermostat__target. That is name mangling. It does not make the attribute truly private, but it does stop casual access and prevents accidental clashes in subclasses.

@property defines the getter and @target.setter defines what happens on assignment. Because __init__ assigns to self.target (not self.__target), the validation runs even at construction time.

The result is that t.target = 26 looks like a plain attribute assignment but goes through a check. Callers get simple syntax; the class keeps its guarantees.

Real-world use #

Duck typing is why Python code often works with "anything file-like" or "anything iterable". A function that accepts an object with .read() works with a real file, a network stream or an in-memory buffer used in tests. That flexibility makes testing much easier.

Properties with validation show up in models and configuration objects everywhere — clamping a percentage, normalising an email, refusing a negative quantity.

Encapsulation pays off during change. If the internal storage of a class changes from a list to a dictionary, code that used only the public methods keeps working. Code that reached into obj._items breaks. That is why the underscore convention matters even without a compiler to enforce it.

Common mistakes #

  • Writing if/elif chains on object type instead of letting each type implement the method.
  • Treating a single underscore as real privacy. It is a signal to humans, not a lock.
  • Reaching into another object’s internals because it was quicker than adding a method.
  • Adding getters and setters for every attribute out of habit. Plain attributes are fine until there is a rule to enforce.
  • Creating a property that does expensive work. Callers expect attribute access to be cheap.

Practice #

Write three classes — EmailChannel, SmsChannel and PushChannel — each with a notify(message) method. Write one function that takes a list of channels and a message and notifies all of them without checking types. Then add a RetryPolicy class whose attempts property refuses any value outside 1 to 5.

Quick quiz

  1. 1. What is duck typing?

  2. 2. What does a single leading underscore mean on an attribute?

  3. 3. What does a double leading underscore trigger?

  4. 4. Why is polymorphism preferable to an if/elif chain on type?

  5. 5. What is the benefit of a property setter?

Summary

  • Polymorphism means the same call behaves correctly for different types.
  • Python uses duck typing — having the method is enough, no shared base class needed.
  • Encapsulation hides internals behind a small, stable public surface.
  • Underscores are conventions; discipline comes from the team, not the language.
  • Properties let you add validation without changing how callers write code.