Programação orientada a objetos em Python: guia com exemplos

Aprenda programação orientada a objetos em Python na prática: classe e objeto, self, encapsulamento, herança e polimorfismo, com código que roda e os erros mais comuns de quem está começando.

Revisado pela equipe editorial ValorFinalDocumentação oficial do Python, Python.org e MDN Web Docs
Calcule agora: JSON FormatterResultado na hora, de graça e sem cadastro.

Programação orientada a objetos (POO) é o jeito de organizar um programa em torno de objetos, que juntam num só lugar os dados e as funções que mexem nesses dados. É o passo que separa quem escreve scripts curtos de quem consegue tocar um projeto maior sem se perder. Este guia mostra a POO em Python na prática, do conceito de classe até dataclasses e MRO, sempre com código que roda: classe e objeto, self, encapsulamento, herança, polimorfismo, abstração, propriedades, métodos mágicos e os erros mais comuns de quem começa. Se quiser estudar com playground no navegador, exercícios corrigidos e certificado, use o curso de Python intermediário, gratuito.

Resposta rápida

  • Classe é o molde; objeto é a peça criada a partir dele. __init__ monta o objeto e self é o próprio objeto dentro dos métodos.
  • Os quatro pilares em Python: encapsulamento, herança (com super()), polimorfismo (duck typing) e abstração (com ABC).
  • O encapsulamento aqui é por convenção: um _ sinaliza uso interno e __ só faz name mangling. Python não tem atributo privado de verdade.
  • Python não obriga a usar POO. Para script simples, função basta; classe vale quando dados e comportamento andam juntos.

O problema que a POO resolve

Imagine controlar uma conta bancária com variáveis soltas. O saldo fica numa variável, e você escreve funções separadas para depositar e sacar, passando o saldo toda hora. Com uma conta só já dá trabalho. Com dez, o código vira um emaranhado de variáveis parecidas (saldo1, saldo2, dono1) e ninguém garante que a função certa mexe no saldo certo. A orientação a objetos resolve isso juntando o dado (o saldo) e o comportamento (depositar, sacar) numa mesma unidade que sabe cuidar de si mesma.

class Conta:
    def __init__(self, dono, saldo=0):
        self.dono = dono
        self.saldo = saldo

    def depositar(self, valor):
        self.saldo += valor

    def sacar(self, valor):
        if valor > self.saldo:
            print("Saldo insuficiente")
        else:
            self.saldo -= valor

conta = Conta("Ana", 100)
conta.depositar(50)
conta.sacar(30)
print(conta.saldo)  # 120

Toda a lógica da conta mora dentro da classe. Quem usa não precisa saber como o saldo é guardado por dentro: chama depositar e sacar e pronto. É a mesma ideia por trás das listas e outros tipos do Python, que também juntam dados e métodos num objeto que você só usa.

Classe e objeto: a planta e a casa

A confusão mais comum de quem começa é misturar classe com objeto. A classe é a planta da casa: um desenho no papel. O objeto é a casa construída a partir da planta. De uma planta você constrói quantas casas quiser, cada uma com sua cor de parede e seus moradores, mas todas seguindo o mesmo desenho.

class Cachorro:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    def latir(self):
        return f"{self.nome} diz: Au!"

rex = Cachorro("Rex", 3)
bob = Cachorro("Bob", 5)
print(rex.latir())   # Rex diz: Au!
print(bob.idade)     # 5

O rex e o bob são dois objetos (ou instâncias) da mesma classe Cachorro. Cada um guarda o próprio nome e a própria idade. O método __init__ é o construtor: ele roda no momento em que você escreve Cachorro("Rex", 3) e prepara o objeto com os valores iniciais. E o self? Ele é o próprio objeto, entregue de graça a cada método. Quando você chama rex.latir(), o Python passa o rex como self por baixo dos panos. Por isso self.nome dentro do método sabe que é o nome do rex, e não do bob.

Atributo de instância x atributo de classe

Existem dois lugares onde um dado pode morar. O atributo de instância nasce dentro do __init__, com self.algo, e é individual: cada objeto tem o seu. O atributo de classe é escrito direto no corpo da classe, fora dos métodos, e é compartilhado por todos os objetos. Serve para valores iguais para todos, como uma taxa padrão ou o número de patas de um cachorro.

Aqui mora uma das armadilhas mais famosas de Python. Se o atributo de classe for uma lista ou um dicionário (um objeto mutável), todos os objetos dividem o mesmo:

class Cachorro:
    patas = 4         # atributo de classe, ok: valor fixo
    truques = []      # ARMADILHA: lista compartilhada por todos

    def __init__(self, nome):
        self.nome = nome

    def ensinar(self, truque):
        self.truques.append(truque)

rex = Cachorro("Rex")
bob = Cachorro("Bob")
rex.ensinar("sentar")
print(bob.truques)    # ['sentar']  <- vazou para o bob!

O rex aprendeu a sentar, mas o truque apareceu na lista do bob. Isso acontece porque truques é um único objeto lista, e self.truques.append alterou essa lista comum. A correção é criar a lista dentro do __init__, com self.truques = [], para que cada cachorro ganhe a sua. Regra de bolso: dado que varia por objeto vai sempre no construtor.

Os quatro pilares em Python

A POO costuma ser resumida em quatro ideias. Não decore os nomes: entenda o problema que cada uma resolve. As próximas seções mostram cada pilar com código de verdade.

PilarO que resolveFerramenta em Python
EncapsulamentoManter dados e comportamento juntos, protegidos de mexida indevida_nome, __nome, @property
HerançaReaproveitar uma classe em outra parecidaclass Filha(Pai), super()
PolimorfismoTratar objetos diferentes pela mesma interfaceduck typing, métodos com o mesmo nome
AbstraçãoEsconder o detalhe e expor só o que importa usarABC, @abstractmethod

Encapsulamento: convenção, não cadeado

No primeiro exemplo, alguém poderia escrever conta.saldo = -999 e furar a regra do saldo. Encapsular é impedir isso, guardando o dado por dentro e oferecendo métodos que controlam o acesso. Aqui está a parte que confunde quem vem do Java: Python não tem atributo privado de verdade. O controle é por combinação, não por trava da linguagem.

A convenção tem dois níveis. Um sublinhado à frente (_saldo) é um recado: uso interno, não mexa direto. A linguagem não impede nada, mas a comunidade respeita. Dois sublinhados (__saldo) ligam o name mangling, um mecanismo que renomeia o atributo para evitar colisão em herança:

class Conta:
    def __init__(self, saldo):
        self.__saldo = saldo    # name mangling: vira _Conta__saldo

c = Conta(100)
# print(c.__saldo)             # AttributeError
print(c._Conta__saldo)         # 100  <- ainda dá para acessar

Repare: o __saldo não sumiu, ele só foi renomeado para _Conta__saldo. Quem souber o nome ainda alcança o valor. O objetivo dos dois sublinhados nunca foi esconder de verdade, e sim evitar que uma classe filha sobrescreva sem querer um atributo do pai. Se você veio de outra linguagem esperando private, ajuste a expectativa: em Python o encapsulamento é um acordo entre pessoas adultas, não uma cerca.

@property: getter e setter pythônico

Se o dado é interno, como validar antes de gravar sem obrigar quem usa a chamar set_saldo() o tempo todo? A resposta é a @property. Ela transforma um método em algo que se usa como se fosse um atributo simples, e deixa você interceptar leitura e escrita:

class Produto:
    def __init__(self, preco):
        self._preco = preco

    @property
    def preco(self):
        return self._preco

    @preco.setter
    def preco(self, valor):
        if valor < 0:
            raise ValueError("Preco nao pode ser negativo")
        self._preco = valor

p = Produto(10)
p.preco = 15         # dispara o setter (valida)
print(p.preco)       # 15  <- dispara o getter

Do lado de fora, p.preco parece um atributo comum, mas por dentro passa pela validação. Você começa com um atributo simples e, no dia em que precisar validar, vira propriedade sem quebrar o código que já usa a classe. Esse é o jeito pythônico: nada de escrever getter e setter para tudo desde o começo, como em outras linguagens.

Herança: reaproveitar com super()

Uma conta corrente é uma conta que aceita usar limite. Em vez de reescrever depósito e saque, você herda de Conta e acrescenta o que muda. O super() chama o construtor do pai para reaproveitar a montagem que ele já faz:

class Conta:
    def __init__(self, dono, saldo=0):
        self.dono = dono
        self.saldo = saldo

class ContaCorrente(Conta):
    def __init__(self, dono, saldo=0, limite=0):
        super().__init__(dono, saldo)   # roda o __init__ do pai
        self.limite = limite

    def sacar(self, valor):
        if valor > self.saldo + self.limite:
            print("Excede o limite")
        else:
            self.saldo -= valor

cc = ContaCorrente("Ana", 100, limite=200)
cc.sacar(250)
print(cc.saldo)   # -150

A ContaCorrente ganha de graça o que a Conta tem, aproveita o construtor com super().__init__(...) e só acrescenta o limite e a nova regra de saque. Use herança quando a frase é um faz sentido (conta corrente é uma conta). Se a relação for tem um, prefira composição, o que veremos adiante.

Polimorfismo e duck typing

Polimorfismo é poder chamar o mesmo método em objetos de tipos diferentes e cada um responder do seu jeito. Em Python isso vem do duck typing, resumido na frase: se anda como pato e faz quack como pato, trate como pato. O Python não checa a classe do objeto antes de chamar o método. Ele só tenta chamar. Se o método existe, funciona:

class Pato:
    def falar(self):
        return "Quack"

class Cachorro:
    def falar(self):
        return "Au"

def apresentar(animal):
    print(animal.falar())   # nao pergunta o tipo, so chama

for a in [Pato(), Cachorro()]:
    apresentar(a)           # Quack / Au

A função apresentar não sabe nem se importa se recebeu um pato ou um cachorro. Ela confia que o objeto tem um método falar. Adicionar um Gato depois não exige mexer na função: basta a classe nova ter o método. É isso que deixa código Python flexível, sem precisar de uma classe pai comum só para agrupar tipos parecidos.

Abstração com ABC: contratos que obrigam

Às vezes você quer garantir que toda classe de uma família implemente certo método. Uma classe base abstrata (ABC, de Abstract Base Class) faz esse contrato. Ela não pode ser instanciada e marca métodos como obrigatórios com @abstractmethod:

from abc import ABC, abstractmethod

class Forma(ABC):
    @abstractmethod
    def area(self):
        ...

class Quadrado(Forma):
    def __init__(self, lado):
        self.lado = lado
    def area(self):
        return self.lado ** 2

# Forma()          # TypeError: nao da para instanciar classe abstrata
q = Quadrado(4)
print(q.area())    # 16

Se você criar uma classe que herda de Forma e esquecer de implementar area, o Python reclama na hora de instanciar, com TypeError. É útil em projeto grande, quando várias pessoas criam formas e você quer garantir que todas tenham area. Para script pequeno, o duck typing sozinho costuma bastar: use ABC quando o contrato precisa ser cobrado.

Métodos dunder (mágicos)

Métodos com dois sublinhados na frente e atrás, como __init__, são chamados de dunder (de double underscore) ou mágicos. O Python os aciona em situações especiais: print(obj) chama __str__, len(obj) chama __len__, obj + outro chama __add__. Definir esses métodos deixa seus objetos se comportarem como os tipos nativos.

MétodoQuando disparaServe para
__init__ao criar o objetomontar o estado inicial
__str__print(obj), str(obj)texto amigável para o usuário
__repr__repr(obj), console, depuradorrepresentação técnica para quem programa
__eq__obj == outrocomparar por valor, não por identidade
__len__len(obj)devolver um tamanho
__add__obj + outrosomar dois objetos
class Dinheiro:
    def __init__(self, centavos):
        self.centavos = centavos

    def __repr__(self):
        return f"Dinheiro({self.centavos})"

    def __str__(self):
        return f"R$ {self.centavos / 100:.2f}"

    def __eq__(self, outro):
        return self.centavos == outro.centavos

    def __add__(self, outro):
        return Dinheiro(self.centavos + outro.centavos)

a = Dinheiro(1050)
b = Dinheiro(200)
print(a + b)                  # R$ 12.50
print(a == Dinheiro(1050))    # True

A diferença entre __str__ e __repr__ confunde muita gente. O __str__ é o texto bonito que print mostra. O __repr__ é a versão técnica que aparece no console e no depurador, e o ideal é que mostre algo capaz de recriar o objeto. Se você definir apenas __repr__, o Python o usa também no lugar do __str__. Por isso, quando estiver com pressa, comece pelo __repr__.

Método de instância, @classmethod e @staticmethod

Nem todo método precisa de um objeto. Existem três tipos, e a diferença está no primeiro parâmetro. O método de instância recebe self e mexe nos dados do objeto. O @classmethod recebe cls (a própria classe) e serve muito como construtor alternativo. O @staticmethod não recebe nada automático: é uma função comum que mora dentro da classe porque tem a ver com o tema.

TipoPrimeiro parâmetroUso típico
Método de instânciaselfcomportamento que usa os dados do objeto
@classmethodclsconstrutor alternativo, cria instâncias
@staticmethodnenhumfunção utilitária ligada ao tema da classe
class Data:
    def __init__(self, dia, mes, ano):
        self.dia = dia
        self.mes = mes
        self.ano = ano

    @classmethod
    def de_string(cls, texto):
        dia, mes, ano = map(int, texto.split("/"))
        return cls(dia, mes, ano)     # cls e a classe Data

    @staticmethod
    def e_bissexto(ano):
        return ano % 4 == 0 and (ano % 100 != 0 or ano % 400 == 0)

d = Data.de_string("25/12/2026")
print(d.dia)                # 25
print(Data.e_bissexto(2024))  # True

O de_string é um segundo jeito de criar uma Data, a partir de texto, e por isso é @classmethod: precisa da classe para chamar cls(...). O e_bissexto só faz uma conta com um ano e não toca em nenhum objeto, então é @staticmethod.

dataclasses: menos código repetido

Muita classe existe só para carregar dados: um ponto, um produto, uma linha de planilha. Escrever __init__, __repr__ e __eq__ na mão para cada uma cansa. O decorador @dataclass, do módulo dataclasses, gera esses três de graça a partir dos campos que você declara:

from dataclasses import dataclass

@dataclass
class Ponto:
    x: int
    y: int

p = Ponto(2, 3)
print(p)                # Ponto(x=2, y=3)  <- __repr__ pronto
print(p == Ponto(2, 3)) # True             <- __eq__ pronto

Sem o decorador, aquele print(p) mostraria algo como <__main__.Ponto object at 0x...> e a comparação daria False mesmo com os mesmos valores. Com @dataclass, você ganha impressão legível e comparação por valor sem escrever nada. Use quando a classe é basicamente um pacote de dados.

Herança múltipla e o MRO

Python permite herdar de mais de uma classe ao mesmo tempo. Isso levanta uma pergunta: se duas classes pai têm o mesmo método, qual vale? A resposta é o MRO (Method Resolution Order, ordem de resolução de métodos), a ordem em que o Python procura o método subindo pela hierarquia:

class A:
    def cumprimentar(self):
        return "A"

class B(A):
    def cumprimentar(self):
        return "B"

class C(A):
    def cumprimentar(self):
        return "C"

class D(B, C):
    pass

d = D()
print(d.cumprimentar())    # B  <- B vem antes de C no MRO
print([cls.__name__ for cls in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']

A classe D herda de B e C, que herdam de A. Pelo MRO, o Python olha primeiro D, depois B, depois C, depois A e, no fim, object (toda classe herda de object). Por isso d.cumprimentar() devolve B. Você consulta essa ordem com D.__mro__. Herança múltipla é poderosa, mas cria essas dúvidas: use com parcimônia e, na dúvida, prefira composição.

Composição x herança: prefira composição

Herança não é a única forma de reaproveitar. Na composição, um objeto guarda outro objeto por dentro em vez de herdar dele. A regra prática, repetida por quem programa há tempo, é: prefira composição a herança. Um carro tem um motor, ele não é um motor, então composição encaixa melhor:

class Motor:
    def ligar(self):
        return "vrum"

class Carro:
    def __init__(self):
        self.motor = Motor()      # o carro TEM um motor

    def dar_partida(self):
        return self.motor.ligar()

c = Carro()
print(c.dar_partida())   # vrum

A vantagem é o baixo acoplamento. Trocar o Motor por um MotorEletrico não obriga a mexer na hierarquia de classes, só a montar o carro com outra peça. Herança amarra a filha ao pai para sempre; composição deixa você trocar as partes. Use herança quando a relação é um é clara e estável; no resto, componha.

Quando não usar POO

Ao contrário de linguagens onde tudo precisa morar dentro de uma classe, o Python não obriga. Um script que lê um arquivo, filtra linhas e imprime um total fica mais claro com funções simples do que embrulhado numa classe que existe só para dar nome a nada. Sinais de que a classe está sobrando: ela tem só um método além do __init__, ou guarda dados que nunca ganham comportamento. Nesses casos, uma função ou um dicionário resolve.

A POO ganha valor quando você percebe grupos de dados que sempre andam juntos e ganham comportamento próprio: um usuário com seus dados e suas ações, um pedido que sabe calcular o próprio total. Se você está aprendendo do zero, vale firmar antes a base de lógica de programação e o conceito de o que é um algoritmo antes de se preocupar com classes.

Erros comuns de quem está começando

O curso de Python intermediário cobre classes, herança, polimorfismo e dataclasses com playground no navegador, exercícios corrigidos e certificado, sem custo. Se você ainda está no começo, comece pelo curso de Python do zero; para ir além, siga no curso de Python avançado. Quem nunca programou pode começar pelo curso de lógica de programação.

Limitações deste guia

Este material é educativo e cobre a base da orientação a objetos em Python até um nível intermediário. Ele não substitui a documentação oficial da linguagem, que traz detalhes de casos mais raros, como métodos especiais menos usados, descritores, metaclasses e regras finas de MRO em hierarquias grandes. Os exemplos foram escritos para rodar em Python 3 e são propositalmente curtos: em código de produção você somaria tratamento de erros, testes e anotações de tipo. Ao aplicar num projeto real, confira sempre o comportamento na versão de Python que você usa.

Fontes oficiais

Conclusão

Orientação a objetos é agrupar dados e comportamento em classes e criar objetos a partir delas. Encapsule para proteger os dados, lembrando que em Python isso é convenção e não cadeado. Use herança e super() só quando existe relação de é um, e prefira composição no resto. Deixe o polimorfismo (duck typing) simplificar o código que trata muitos tipos, cobre contratos com ABC quando precisar, e economize código repetido com @dataclass. A melhor forma de fixar é escrever suas próprias classes e testar cada conceito. Para praticar com playground, exercícios e certificado gratuitos, use o curso de Python intermediário. Depois, aprofunde em decorators em Python e em estruturas de dados e algoritmos, explore o guia de como aprender Python do zero e as estruturas de dados avançadas. Ferramentas como o formatador de JSON e o gerador de hash ajudam no dia a dia de quem programa. Veja também todos os cursos gratuitos, a categoria Tecnologia e como validamos os conteúdos.

Calculadoras deste guia

Leia também

Fontes oficiais

Links externos para os documentos oficiais consultados na construção desta página. O conteúdo deles pode mudar sem aviso; em caso de divergência, vale sempre a fonte oficial.

Como validamos os cálculos

Os valores citados neste guia são estimativos e baseados em fontes oficiais (Documentação oficial do Python, Python.org e MDN Web Docs). Eles podem variar conforme convenção coletiva, situação individual e atualizações da legislação. Entenda nossa metodologia em como validamos os cálculos.

Perguntas frequentes

O que é programação orientada a objetos em poucas palavras?
É um jeito de organizar o programa em torno de objetos: pequenos pacotes que juntam dados e as funções que mexem nesses dados. Em vez de espalhar variáveis soltas e funções pelo código, você agrupa tudo o que pertence a uma mesma coisa. Um objeto Conta, por exemplo, guarda o saldo e sabe como depositar e sacar. Isso deixa programas maiores mais fáceis de entender e de mudar.
Qual a diferença entre classe e objeto?
A classe é o molde e o objeto é a peça feita a partir dele. A classe Cachorro descreve o que todo cachorro tem (nome, idade) e faz (latir). Cada cachorro concreto que você cria a partir dela é um objeto: o rex e o bob são dois objetos da mesma classe, cada um com seus próprios valores. Você escreve a classe uma vez e cria quantos objetos precisar.
O que é self em Python?
self é o próprio objeto, passado automaticamente para os métodos. Quando você escreve def depositar(self, valor), o self permite que o método leia e altere os dados daquele objeto específico, como self.saldo. Sem ele, o método não saberia de qual conta está falando. Você não passa self na chamada: o Python coloca ali sozinho o objeto que está antes do ponto.
Qual a diferença entre atributo de instância e atributo de classe?
O atributo de instância pertence a cada objeto e é criado dentro do __init__ com self.nome. O atributo de classe é escrito direto no corpo da classe e é compartilhado por todos os objetos. Use atributo de classe para valores fixos e iguais para todos, como uma taxa padrão. A armadilha clássica é usar uma lista ou dicionário como atributo de classe: como é o mesmo objeto para todos, mexer em um afeta todos. Dados que variam por objeto vão sempre no __init__.
Em Python o atributo privado com dois sublinhados é realmente privado?
Não. Python não tem atributo privado de verdade como Java ou C#. Um nome com dois sublinhados à frente, como __saldo, passa por name mangling: o interpretador o renomeia para _Conta__saldo. Isso evita colisão de nomes em herança, mas não bloqueia o acesso. Quem souber o nome mangleado ainda alcança o valor. O encapsulamento em Python é por convenção: um sublinhado (_saldo) diz uso interno, e a comunidade respeita isso sem que a linguagem force.
O que é herança e quando usar?
Herança é quando uma classe aproveita o que outra já tem e acrescenta ou muda alguma coisa. Uma classe ContaPoupanca pode herdar de Conta e adicionar o cálculo de rendimento, sem reescrever depósito e saque. Use quando existe de fato uma relação do tipo é um (poupança é uma conta). Quando a relação é tem um, composição costuma ser melhor: um Carro tem um Motor, não herda de motor.
O que é super() e para que serve?
super() dá acesso ao método da classe de cima na hierarquia. O uso mais comum é dentro do __init__ da classe filha: super().__init__(...) roda o construtor da classe pai para aproveitar a montagem que ela já faz, e só depois a filha acrescenta o que é dela. Assim você não copia o código do pai. Em herança múltipla, super() segue a ordem do MRO, não simplesmente a primeira classe pai.
O que é duck typing?
Duck typing é a forma de polimorfismo do Python. A ideia vem da frase se anda como pato e faz quack como pato, trate como pato. O Python não verifica o tipo do objeto antes de chamar um método: ele só tenta chamar. Se o objeto tem o método esperado, funciona, não importa a classe. Isso deixa o código flexível: qualquer objeto com um método falar() serve, sem precisar herdar de uma classe comum.
Qual a diferença entre __str__ e __repr__?
__str__ devolve o texto amigável, para o usuário final, e é o que print() usa. __repr__ devolve a representação técnica, para quem programa, e aparece no console interativo e no depurador. A regra prática: __repr__ deveria, quando possível, mostrar algo que recria o objeto, como Dinheiro(1050). Se você definir só __repr__, o Python usa ele também no lugar do __str__. O contrário não vale: __str__ não cobre o __repr__.
Quando usar @classmethod e quando usar @staticmethod?
Use @classmethod quando o método precisa da classe, não do objeto, sendo o caso mais comum um construtor alternativo, como Data.de_string('25/12/2026'). Ele recebe cls e pode criar instâncias. Use @staticmethod para uma função utilitária que tem a ver com o tema da classe mas não usa nem o objeto nem a classe, como verificar se um ano é bissexto. Método de instância normal, com self, é para tudo que depende dos dados daquele objeto.
O que é dataclass e quando vale a pena?
dataclass é um decorador do módulo dataclasses que gera automaticamente __init__, __repr__ e __eq__ a partir dos campos que você declara com anotação de tipo. Vale a pena quando a classe existe basicamente para guardar dados, o caso de um Ponto, um Produto ou uma linha de planilha. Você escreve x: int, y: int e ganha o construtor, a impressão legível e a comparação por valor de graça, cortando o código repetitivo.
Preciso usar orientação a objetos em todo programa Python?
Não. Scripts curtos, que só leem dados e fazem uma conta, ficam ótimos com funções simples. A orientação a objetos ganha valor quando o programa cresce e você percebe grupos de dados que sempre andam juntos e ganham comportamento próprio: um usuário, um pedido, um produto. Muitos programas misturam os dois estilos, e isso é normal. O Python não obriga a criar classes, ao contrário de linguagens onde tudo precisa morar dentro de uma.
Onde pratico orientação a objetos em Python sem instalar nada?
Dá para rodar tudo no navegador. O curso de Python intermediário do ValorFinal tem um playground que executa o código na própria página, então você cria classes, objetos e herança e vê o resultado na hora, sem configurar ambiente. É a forma mais rápida de fixar cada conceito escrevendo e testando.