Encapsulation, abstraction, polymorphism, and multiple inheritance — the four pillars
These four pillars are the vocabulary of object-oriented system design. They determine whether you can design software, not just write it. Encapsulation tells you how to hide complexity; abstraction how to define contracts; polymorphism how to write code that works with types you haven't seen yet; inheritance how to build specialisation on top of existing behaviour without duplicating it.
Encapsulation — expose a clean interface, hide the internals. Abstraction — define what must be done, not how. Polymorphism — same call, different behaviour per type. Inheritance — build specialised classes on top of existing ones without rewriting what already works.
Encapsulation bundles data and behaviour together and controls access to internals. Python uses _ (protected by convention) and __ (name mangling) to signal access levels. @property is the Pythonic way to add validation without changing the attribute access syntax callers use.
_name is a convention — Python doesn't enforce it, but it signals "internal, don't rely on this." __name triggers name mangling: the attribute is renamed to _ClassName__name, preventing accidental overrides in subclasses.
Abstraction hides implementation complexity behind a clean interface. An abstract base class defines what subclasses must implement — @abstractmethod enforces this at instantiation. You cannot create an instance of an abstract class directly.
Any subclass that forgets to implement an abstract method raises TypeError at instantiation — not at the call site. Bugs surface early, at the point of definition rather than deep inside production code.
Polymorphism means the same operation behaves differently depending on the type. Python supports two forms: method overriding (subclass replaces the parent's method) and duck typing (any object with the right method works, regardless of inheritance).
"If it has the right method, it works" — Python doesn't require a shared base class. Duck typing makes polymorphism more flexible than in statically-typed languages. Use Protocol (from typing) to make the expectation explicit for tooling without forcing inheritance.
Inheritance lets a class acquire properties and methods of another class — building specialisation on top of existing behaviour without duplicating it. It's the "is-a" relationship: a Dog is an Animal. super() calls the parent's version of a method, keeping the chain intact.
Always call super().__init__() in subclass constructors — skipping it leaves parent state uninitialised. Use isinstance over type(x) == Y; it respects the full inheritance chain.