Este contenido solo está disponible en Inglés.
Aún sin traducción para este idioma.
Object-Oriented Programming in Python: for those who got stuck trying to learn it
Have you ever felt like you understood the words of OOP, but understood nothing? It's not a lack of talent. Someone gave you the key before showing you the lock. This article corrects that, revealing the real problem that Object-Oriented Programming in Python solves. Put syntax aside and understand why classes, inheritance, encapsulation, and polymorphism exist, with practical examples.
You didn't get stuck because OOP is difficult. You got stuck because no one showed you the problem before giving you the solution. This article fixes that — for real.
First, an honest question
Have you ever reached the "object-oriented programming" part of a tutorial and felt that specific thing — the sensation that you understood the words but understood nothing?
You read: "a class is a blueprint for creating objects". It made sense on the surface. But that unanswered question remained: why would I need this? When would I use it? My code works without it...
This feeling isn't a lack of talent. It's a sign that someone handed you the key before showing you the lock.

The idea of the lock and key: it makes no sense to create a key and go looking for a lock. The right way is to find a real lock — a problem you already feel — and then the key makes perfect sense.
So let's start with the problem. Not the solution.
The problem you'll soon feel

Imagine you're building a system for a pet shop. Small, just for learning. You start like this:
# Looks ok at first...
gato1_nome = "Whiskers"
gato1_idade = 3
gato1_dono = "Ana"
cachorro1_nome = "Rex"
cachorro1_idade = 5
cachorro1_dono = "Bruno"It works. For now. But then you add more animals. And more. And you start needing functions. And to update data. And anytime you look at the code, there's a background noise saying: this isn't right.
That noise has a name: lack of structure. And that's exactly where OOP exists to help you.
Not to make the code "more elegant". But to solve this specific pain you just imagined — and which you'll truly feel in your next project.
The class: a blueprint that makes sense
A class is a way of telling Python: "this set of data and behaviors go together, and they have a name."
Think of an animal's registration form. It always has a name, age, and owner. And it can always be updated, queried, printed. The class is that form — and each animal you register is a filled-out copy of it.
class Animal:
def __init__(self, nome, idade, dono):
# __init__ runs automatically when you create the animal
self.nome = nome
self.idade = idade
self.dono = dono
def apresentar(self):
print(f"{self.nome}, {self.idade} anos — dono: {self.dono}")
# Now creating 100 animals is simple, clean, and consistent
gato = Animal("Whiskers", 3, "Ana")
cachorro = Animal("Rex", 5, "Bruno")
gato.apresentar() # Whiskers, 3 anos — dono: Ana
cachorro.apresentar() # Rex, 5 anos — dono: BrunoAbout self: it's Python's way of saying "myself". When you call gato.apresentar(), Python automatically passes the gato object itself as the first argument. You declare it, but you don't pass it manually. It seems strange at first — it becomes natural in hours.
The four pillars — no mystery
OOP has four concepts that appear everywhere. Before seeing the code, understand why each one exists:
01
Inheritance
Avoid rewriting what already exists. Child inherits from parent.
02
Encapsulation
Protect internal data. Expose only what needs to be exposed.
03
Polymorphism
Same call, different results. Flexible code.
04
Abstraction
Hide complexity. Show only the essentials to the user.
Inheritance — because repeating code is a trap
You have Animal. Now you want to create Gato (Cat) and Cachorro (Dog) with specific behaviors — but without rewriting name, age, and owner again. Inheritance solves this:
class Gato(Animal): # inherits everything from Animal
def falar(self):
print(f"{self.nome} diz: miau 🐱")
def apresentar(self): # overrides the parent's method
super().apresentar() # still calls the parent's
print("...I'm very independent, by the way. 😼")
class Cachorro(Animal):
def falar(self):
print(f"{self.nome} diz: au au 🐶")
whiskers = Gato("Whiskers", 3, "Ana")
rex = Cachorro("Rex", 5, "Bruno")
whiskers.apresentar() # uses the overridden apresentar
rex.falar() # Rex diz: au au 🐶Encapsulation — because not everything should be accessed directly
Think of a bank account. The balance exists, but no one from outside should be able to write account.balance = 999999 directly. Encapsulation creates this protection:
class ContaBancaria:
def __init__(self, titular, saldo):
self.titular = titular
self.__saldo = saldo # __ = private, no direct access
def depositar(self, valor):
if valor > 0:
self.__saldo += valor
print(f"Deposit of R${valor:.2f} made ✅")
def sacar(self, valor):
if valor > self.__saldo:
print("Insufficient balance ❌")
else:
self.__saldo -= valor
print(f"Withdrawal of R${valor:.2f} made ✅")
def ver_saldo(self):
return f"R${self.__saldo:.2f}"
conta = ContaBancaria("Ana", 1000)
conta.depositar(500)
conta.sacar(200)
print(conta.ver_saldo()) # R$1300.00
# conta.__saldo → AttributeError. Protected. 🔒Detail that makes a difference: __attribute (two underscores) truly blocks external access — Python renames it internally. _attribute (one underscore) is just a convention: "use with care", but it doesn't technically block access.
Polymorphism — the same call, each object responding in its own way

This is the part that seems magical when you understand it. You call .enviar() (send) on any object in the system — and each one knows what to do:
class Notificacao:
def __init__(self, destino, mensagem):
self.destino = destino
self._mensagem = mensagem
def enviar(self):
raise NotImplementedError("Implement in subclasses")
class Email(Notificacao):
def enviar(self):
print(f"📧 Email → {self.destino}: {self._mensagem}")
class SMS(Notificacao):
def enviar(self):
print(f"📱 SMS → {self.destino}: {self._mensagem}")
class Push(Notificacao):
def enviar(self):
print(f"🔔 Push → {self.destino}: {self._mensagem}")
# Polymorphism: same function, three different behaviors
def disparar(fila):
for n in fila:
n.enviar()
disparar([
Email("ana@email.com", "New article on Blueprint Blog!"),
SMS("+55 22 99999-0000", "Check it out 📲"),
Push("user-42", "Don't miss this 🔥"),
])Why this matters: tomorrow you want to add WhatsApp. You create class WhatsApp(Notificacao), implement enviar(), and add it to the queue. The rest of the code doesn't change at all. This is the real power of polymorphism.
You learned OOP. But what did you really gain?
It wasn't just new syntax. You gained a way of thinking about code that will change how you organize any project from now on.
When you look at a problem now, you start asking: what are the entities here? What do they have in common? What is specific to each one? This reasoning is what separates code that lasts from code you abandon.
Key Takeaways
- A class is the blueprint. An object is what you create from it.
- __init__ initializes attributes. self is the reference to the object itself.
- Inheritance: child inherits from parent, without repeating code.
- Encapsulation: protects data with __ or _. Controls access.
- Polymorphism: same call, each class responds differently.
- OOP is not about elegance. It's about not getting lost in your own code as it grows.



