Este conteúdo está disponível apenas em Inglês.

Também está disponível em Português.

Ver tradução
Programação & Dev

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.

Equipe Blueprintblog8 min
Object-Oriented Programming in Python: for those who got stuck trying to learn it

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.

A metaphor showing a hand holding a shiny, complex key, but looking confused or frustrated because there's no lock visible. The key looks out of place or useless without its corresponding problem. Emphasize the missing context. flat design, dark background, no text, no labels.
A metaphor showing a hand holding a shiny, complex key, but looking confused or frustrated because there's no lock visible. The key looks out of place or useless without its corresponding problem. Emphasize the missing context. flat design, dark background, no text, no labels.

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

A visual representation of messy, unstructured data. Multiple colored boxes or files are scattered haphazardly, connected by tangled, broken lines, symbolizing a lack of organization and increasing complexity in a system. flat design, dark background, no text, no labels.
A visual representation of messy, unstructured data. Multiple colored boxes or files are scattered haphazardly, connected by tangled, broken lines, symbolizing a lack of organization and increasing complexity in a system. flat design, dark background, no text, no labels.

Imagine you're building a system for a pet shop. Small, just for learning. You start like this:

text
# 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.

python
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: Bruno

About 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:

python
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:

python
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

An illustration showing a single button labeled 'send' being pressed. From this single action, three different visual outcomes radiate: an email icon, an SMS message bubble, and a push notification bell, each with a distinct visual representation of its message being delivered. This demonstrates multiple responses from a single action. flat design, dark background, no text, no labels.
An illustration showing a single button labeled 'send' being pressed. From this single action, three different visual outcomes radiate: an email icon, an SMS message bubble, and a push notification bell, each with a distinct visual representation of its message being delivered. This demonstrates multiple responses from a single action. flat design, dark background, no text, no labels.

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:

python
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.

Tags do artigo

Artigos relacionados

Receba os ultimos artigos no seu email.

Follow Us: