
I spent two semesters at NTNU learning object oriented programming, and the syntax was the easy part. The hard part was learning to think about programs in a different way. With procedural code I’d think in terms of “first do this, then that, then this other thing.” OOP wanted me to think about what things existed in my program and how they talked to each other. That took a while to internalize.
A different mental model
The example that finally made it click for me was something close to this:
1 | from dataclasses import dataclass |
Once I started writing code like this, the question I was asking changed. Instead of “how do I track task completion,” I started asking “what does it mean for something to be a Task, and what should Tasks be able to do?” That second question scales much better as the program grows.
Inheritance
The classic animal kingdom example is a cliché for a reason. It happens to be a clean way to show how inheritance and polymorphism actually work:

1 | from abc import ABC, abstractmethod |
The thing that took me longer to internalize is that inheritance is often the wrong tool. Most relationships in real systems look like “this thing has a thing” rather than “this thing is a thing,” and composition will usually serve you better than a class hierarchy. I built more than one tangled hierarchy before that lesson sank in.
SOLID
I rolled my eyes at the SOLID principles when they came up in lectures. They sounded like a list of academic things you say in interviews. After enough refactors I started to see why they exist.
The single responsibility principle is the one I think about most. Each class should do one thing. When I extracted a notification service out of a TaskManager, the rest of the code got noticeably easier to read:
1 | from typing import Protocol |
The TaskManager handles tasks. The NotificationService handles notifications. When I want to swap the notification logic for something else, like a Slack integration or an email gateway, I don’t have to touch the TaskManager at all.
Design patterns
I memorized design patterns from a textbook before I really understood any of them. They felt like trivia, the kind of thing you read once and forget. The patterns started making sense only after I’d run into the exact problems they were meant to solve and reinvented bad versions of them.
The observer pattern, for example, fits naturally for notifications:

1 | from abc import ABC, abstractmethod |
Once you’ve written that kind of subscription logic three times by hand, the pattern stops being a name from a textbook and starts being the obvious shape of the problem.
A few things I’d tell my earlier self
The relationships between objects matter more than the objects themselves. I used to spend a lot of time perfecting individual classes when the real complexity was always in the seams between them.
You learn OOP by writing bad OOP code first and then refactoring it. The principles are easier to appreciate after you’ve built something messy enough to need them.
Most of what I learned came together when we built Recess Chess, a chess engine I worked on with classmates. Pieces, moves, board states, and player turns are exactly the kind of system where good OOP shines. Build a couple of things at that scale and the rest stops feeling abstract.