Classes & OOP in Python
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This cover sets the stakes for the 'why' angle. Having defined classes, the question becomes whether OOP is worth adopting over plain functions and dictionaries. The hook — functions on loose data scale until data and rules drift apart — previews the central argument: classes keep related things together so bugs have fewer places to hide.
The lock emoji and the framing around maintainable codebases versus a pile of helpers position this post as the bridge between knowing the syntax and actually reaching for a class when the problem calls for one.
The lead reason is the most durable argument for OOP: when data and the rules that govern it live in the same place, the rules can't be bypassed. If the only way to change a balance is through deposit() and withdraw(), those methods can enforce that it never goes negative. Scatter the same logic across free functions and a single stray assignment can corrupt the state.
There's a correctness dimension here that parallels the declarative argument from earlier days. Bundling state with its guardian methods narrows the surface where invalid states can arise to exactly the methods you wrote to prevent them. Fewer entry points to the data means fewer ways for it to go wrong.
This code slide grounds the abstract argument in a concrete before-and-after. The loose dictionary version has nothing stopping a caller from setting the balance to a nonsensical negative value — the data is naked. The Account class hides the balance behind a withdraw method that checks the rule before allowing the change.
Reading the two versions, notice where the safety comes from: in the object version, the invariant 'never overdraw' is enforced at the single point where the change happens. The underscore on _balance signals 'internal, don't touch directly,' a convention the 'how' post revisits. The lesson is that the object doesn't just store the balance — it owns the rules about it.
The encapsulation point introduces the precise term for what the previous slide demonstrated: an invariant is a truth that must always hold for an object to be valid. Balance never negative, a collection never holding duplicates, a connection always either open or closed. Encapsulation is the technique of hiding the raw data and exposing only methods that preserve those truths.
The practical payoff is that callers literally cannot put the object into an illegal state, because they never touch the internals directly — they go through methods that check and maintain the invariants. This is the difference between hoping every caller remembers the rules and building an object that enforces them no matter who calls it.
The inheritance argument tackles code reuse at scale. When many classes share most of their behavior and differ only in details, inheritance lets the shared logic live once in a base class while each subclass overrides just the part that's different. A bug fix or improvement in the base immediately propagates to every subclass.
The alternative — copying shared logic into each class — is the source of a whole category of maintenance pain. Every copy is a place the bug also lives, and fixing nine of ten copies while missing the tenth is exactly the kind of silent inconsistency that ships. Inheritance establishes a single source of truth for shared behavior, which the comparison slide makes visual.
The comparison directly contrasts copy-paste reuse with inheritance. On the left, the same logic duplicated across N classes means fixing a bug N times, code that drifts apart over edits, and the ever-present risk of missing a copy. On the right, the logic lives once in a base, a single fix benefits everyone, subclasses carry only their differences, and there's one source of truth.
Putting them side by side makes the maintenance argument concrete. The danger of duplication isn't visible on day one — it surfaces months later when the copies have quietly diverged. Inheritance front-loads a little structure to avoid that slow-motion failure, which is why it's a core reason OOP scales better than loose helpers as a codebase grows.
Polymorphism is the most powerful and least obvious benefit, so this slide makes it concrete: if every shape exposes an .area() method, a function can sum the areas of a mixed list of shapes without knowing or caring whether each is a circle or a square. The code talks to an interface, not a concrete type.
The payoff is extensibility. Add a Triangle class later with its own area() and the summing function needs no change at all — it already works with anything that has an area. Code written against an interface bends to accommodate new types instead of breaking, which is exactly how OOP systems absorb growing requirements without constant rewrites of the callers.
This code slide proves polymorphism with the minimal example. Circle and Square are unrelated classes that happen to share one thing: both define an area() method. The final line sums the areas of a list containing both, and the summing code never branches on type — it just calls .area() on each.
The deeper lesson is in what's absent: there's no if-isinstance ladder, no type checking. Python's duck typing means 'if it has an area() method, it counts as a shape here.' Running this and then adding a third unrelated class with its own area() — and watching the sum just work — is the clearest way to feel why polymorphism makes code resilient to new requirements.
The comparison draws the boundary honestly, which is essential for a balanced 'why' post. Reach for a class when data and behavior travel together, when you have many instances each carrying their own state, when there's shared behavior to specialize, or an invariant to protect. Reach for a plain function when the work is a stateless transform, a one-off computation, or when you only have data with no behavior to attach.
Giving explicit 'use a function instead' cases prevents the cargo-cult overuse that the closing mistake warns about. A reader who internalizes both columns will reach for classes confidently where they earn their keep and not feel obligated to wrap stateless logic in a class out of habit.
The 'where you'll reach for OOP' bullets ground the abstract case in the situations that recur constantly: domain models like User, Order, and Account; GUI widgets and game entities that each carry state; plugins sharing a common base interface; machine-learning estimators with their fit/predict pattern; and anything with a lifecycle of state changes.
Seeing the list makes the value tangible — these aren't edge cases, they're the backbone of real applications. The ML estimator example is a deliberate nod to the series' theme: scikit-learn's whole API is objects with fit() and predict() methods, which is why understanding classes is a prerequisite for understanding the tools the later AI content builds on.
The closing mistake is the natural counterweight to a post praising OOP: don't wrap everything in a class out of reflex. A module of pure functions is frequently clearer than a class that has one method and no real state to protect. If your class has a single method and no attributes worth guarding, it wanted to be a function.
The principle to hold onto is that OOP is a tool matched to a shape of problem — data and behavior that genuinely belong together — not a default to apply everywhere. Programmers coming from languages that force everything into classes often over-apply the pattern in Python, which happily mixes functions and objects. This sets up the mistakes post, where over-engineering with classes is examined alongside the more mechanical bugs.
This CTA bridges from why OOP matters to how it actually works. Having argued for adopting classes where data and behavior belong together, the next step is understanding the machinery — object creation, attribute lookup, method resolution — so you can use classes correctly rather than just enthusiastically.
The teaser promises an under-the-hood walkthrough of how Python creates objects, resolves attributes, and finds methods. That precision is what prevents the shared-state and inheritance bugs the later posts cover.