Os métodos mágicos (também chamados de dunder methods, abreviação de "double underscore") são métodos especiais do Python que começam e terminam com dois underscores. Eles permitem que suas classes se comportem como tipos nativos da linguagem, respondendo a operadores, chamadas de função, iteração, indexação e muito mais. Dominar esses métodos é o que separa um desenvolvedor Python mediano de um verdadeiro mestre da linguagem.

Neste guia completo, você vai aprender os métodos mágicos mais importantes da linguagem, desde os fundamentos da criação de objetos até técnicas avançadas de personalização. Cada seção inclui exemplos práticos que você pode usar imediatamente nos seus projetos.

O Que São Métodos Mágicos?

Métodos mágicos são a implementação Python do que outras linguagens chamam de sobrecarga de operadores e polimorfismo. Quando você escreve obj + outro_obj ou len(colecao), o Python internamente procura métodos específicos na classe desses objetos. O interpretador converte operações comuns em chamadas de métodos com nomes previsíveis, definidos na documentação oficial de métodos especiais do Python.

Por exemplo, quando você usa +, o Python chama __add__. Quando usa len(), chama __len__. Isso significa que você pode fazer suas próprias classes responderem a essas mesmas operações simplesmente implementando os métodos apropriados.

Métodos de Criação e Destruição de Objetos

__new__ e __init__

O __new__ é o verdadeiro construtor do Python — ele cria e retorna uma nova instância da classe. Já o __init__ é o inicializador, responsável por configurar o estado do objeto recém-criado. Enquanto __init__ é amplamente utilizado, __new__ é mais comum em padrões como Singleton ou quando se trabalha com classes imutáveis.

class Singleton:
    _instance = None
def __new__(cls, *args, **kwargs):
    if cls._instance is None:
        cls._instance = super().__new__(cls)
    return cls._instance

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

a = Singleton(10) b = Singleton(20) print(a is b) # True print(a.valor) # 20

__del__

O método __del__ é chamado quando o objeto está prestes a ser destruído pelo garbage collector. Use com cautela, pois não há garantia de quando ele será executado. É útil para liberar recursos externos como arquivos ou conexões, mas o gerenciamento explícito com context managers (veja __enter__ e __exit__) é quase sempre preferível.

Representação de Strings

__str__ e __repr__

Estes são provavelmente os métodos mágicos mais conhecidos e utilizados. __repr__ deve retornar uma representação "oficial" e não ambígua do objeto, idealmente uma string que recrie o objeto. __str__ retorna uma representação "informal" e legível para o usuário final. Se __str__ não for implementado, o Python usa __repr__ como fallback.

class Usuario:
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email
def __repr__(self):
    return f"Usuario(nome='{self.nome}', email='{self.email}')"

def __str__(self):
    return f"{self.nome} <{self.email}>"

user = Usuario("Maria Silva", "[email protected]") print(repr(user)) # Usuario(nome='Maria Silva', email='[email protected]') print(str(user)) # Maria Silva <[email protected]>

A Real Python tem um guia excelente sobre métodos mágicos que aprofunda ainda mais esses conceitos.

__format__

O método __format__ permite personalizar como o objeto se comporta dentro de f-strings e com a função format(). Você pode criar especificações de formato customizadas:

class Moeda:
    simbolos = {"BRL": "R$", "USD": "$", "EUR": "€"}
def __init__(self, valor, moeda="BRL"):
    self.valor = valor
    self.moeda = moeda

def __format__(self, spec):
    simb = self.simbolos.get(self.moeda, self.moeda)
    if spec == "extenso":
        return f"{simb} {self.valor:,.2f} ({self.moeda})"
    return f"{simb} {self.valor:{spec}f}" if spec else f"{simb} {self.valor:,.2f}"

dinheiro = Moeda(1250.50) print(f"{dinheiro}") # R$ 1,250.50 print(f"{dinheiro:extenso}") # R$ 1,250.50 (BRL)

Métodos de Comparação

Os métodos de comparação permitem que objetos sejam comparados com operadores como ==, <, >, <=, >= e !=. Implementá-los corretamente também habilita a ordenação com sorted().

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade
def __eq__(self, other):
    if not isinstance(other, Pessoa):
        return NotImplemented
    return self.nome == other.nome and self.idade == other.idade

def __lt__(self, other):
    if not isinstance(other, Pessoa):
        return NotImplemented
    return self.idade &lt; other.idade

def __hash__(self):
    return hash((self.nome, self.idade))

def __repr__(self):
    return f"{self.nome} ({self.idade})"

pessoas = [ Pessoa("Ana", 30), Pessoa("Bruno", 25), Pessoa("Carla", 35), ] for p in sorted(pessoas): print(p) # Bruno (25), Ana (30), Carla (35)

Implementar __eq__ e __hash__ juntos é essencial se você pretende usar seus objetos em conjuntos (set) ou como chaves de dicionários. A GeeksforGeeks tem uma referência completa sobre dunder methods com mais exemplos desses padrões.

Métodos Aritméticos

O Python permite sobrecarregar praticamente todos os operadores aritméticos. Isso é especialmente útil para criar tipos numéricos personalizados, vetores, matrizes e domínios específicos.

class Vetor:
    def __init__(self, x, y):
        self.x = x
        self.y = y
def __add__(self, other):
    if not isinstance(other, Vetor):
        return NotImplemented
    return Vetor(self.x + other.x, self.y + other.y)

def __sub__(self, other):
    if not isinstance(other, Vetor):
        return NotImplemented
    return Vetor(self.x - other.x, self.y - other.y)

def __mul__(self, escalar):
    if not isinstance(escalar, (int, float)):
        return NotImplemented
    return Vetor(self.x * escalar, self.y * escalar)

def __abs__(self):
    return (self.x ** 2 + self.y ** 2) ** 0.5

def __repr__(self):
    return f"Vetor({self.x}, {self.y})"

v1 = Vetor(3, 4) v2 = Vetor(1, 2) print(v1 + v2) # Vetor(4, 6) print(v1 * 2) # Vetor(6, 8) print(abs(v1)) # 5.0

Além dos operadores básicos, existem métodos para operadores compostos como __iadd__ (+=), __isub__ (-=), operadores unários como __neg__ e __pos__, e operadores de comparação inversa. Consulte o módulo operator do Python para a lista completa.

Métodos de Container

Com estes métodos, você faz suas classes se comportarem como listas, dicionários ou conjuntos. Eles são a base do protocolo de containers em Python.

class ListaPersonalizada:
    def __init__(self, *itens):
        self._itens = list(itens)
def __len__(self):
    return len(self._itens)

def __getitem__(self, index):
    return self._itens[index]

def __setitem__(self, index, valor):
    self._itens[index] = valor

def __delitem__(self, index):
    del self._itens[index]

def __contains__(self, item):
    return item in self._itens

def __iter__(self):
    return iter(self._itens)

def __reversed__(self):
    return reversed(self._itens)

def __repr__(self):
    return f"ListaPersonalizada({self._itens!r})"

lista = ListaPersonalizada(10, 20, 30, 40, 50) print(len(lista)) # 5 print(30 in lista) # True print(lista[1:3]) # [20, 30] lista[0] = 100 print(lista) # ListaPersonalizada([100, 20, 30, 40, 50])

Implementar o protocolo de container completo faz com que sua classe funcione perfeitamente com loops for, compreensão de listas e a maioria das funções built-in que esperam sequências.

Objetos Chamáveis com __call__

O método __call__ permite que instâncias de uma classe sejam chamadas como funções. É extremamente útil para criar funções com estado (closures com classe), decoradores baseados em classe e fábricas.

from time import perf_counter

class Temporizador: def init(self): self.tempos = []

def __call__(self, func):
    import functools
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        inicio = perf_counter()
        resultado = func(*args, **kwargs)
        fim = perf_counter()
        self.tempos.append((func.__name__, fim - inicio))
        print(f"{func.__name__} executou em {fim - inicio:.4f}s")
        return resultado
    return wrapper

timer = Temporizador()

@timer def processar_dados(): return sum(range(1_000_000))

@timer def outra_funcao(): return sum(i * 2 for i in range(500_000))

processar_dados() outra_funcao() print(timer.tempos)

Objetos chamáveis são uma alternativa elegante a funções em muitos cenários, especialmente quando você precisa manter estado entre chamadas.

Context Managers: __enter__ e __exit__

Os context managers permitem usar a sintaxe with para gerenciar recursos. Implementar __enter__ e __exit__ na sua classe permite criar context managers personalizados para conexões de banco de dados, arquivos, transações e muito mais.

class ConexaoBanco:
    def __init__(self, string_conexao):
        self.string_conexao = string_conexao
        self.conexao = None
def __enter__(self):
    print(f"Conectando a {self.string_conexao}...")
    self.conexao = {"conectado": True, "url": self.string_conexao}
    return self.conexao

def __exit__(self, exc_type, exc_val, exc_tb):
    if self.conexao:
        print("Fechando conexão...")
        self.conexao["conectado"] = False
    if exc_type:
        print(f"Erro tratado: {exc_val}")
    return True  # Suprime exceções

with ConexaoBanco("postgresql://localhost:5432/mydb") as conn: print("Operações com banco de dados...") raise ValueError("Algo deu errado!")

print("Continuou após a exceção tratada")

O retorno True em __exit__ suprime exceções. Se você retornar False ou None, a exceção se propaga. Essa é uma das funcionalidades mais importantes para escrever código robusto e idiomático em Python.

Controle de Acesso a Atributos

Os métodos __getattr__, __setattr__, __delattr__ e __getattribute__ oferecem controle fino sobre como os atributos são acessados e modificados. Use __getattr__ para fornecer valores padrão para atributos inexistentes e __setattr__ para validar ou transformar valores antes de armazená-los.

class Validado:
    def __init__(self):
        self.__dict__["_dados"] = {}
def __getattr__(self, nome):
    if nome in self._dados:
        return self._dados[nome]
    raise AttributeError(f"'{type(self).__name__}' não tem atributo '{nome}'")

def __setattr__(self, nome, valor):
    if nome.startswith("_"):
        self.__dict__[nome] = valor
    elif isinstance(valor, (int, float)) and valor &lt; 0:
        raise ValueError(f"{nome} não pode ser negativo")
    else:
        self._dados[nome] = valor

obj = Validado() obj.nome = "Python" obj.idade = 25 print(obj.nome) # Python

obj.idade = -5 # ValueError: idade não pode ser negativo

Cuidado com loops infinitos ao usar __setattr__ — use self.__dict__ diretamente para evitar recursão. A documentação oficial sobre acesso a atributos detalha todas as nuances desse mecanismo.

Conversão de Tipo: __int__, __float__, __bool__

Estes métodos permitem que seus objetos sejam convertidos para tipos nativos usando funções como int(), float() e bool(). O método __bool__ é particularmente importante, pois determina como o objeto se comporta em contextos booleanos como if, while e operadores lógicos.

class Pontuacao:
    def __init__(self, valor, maximo=100):
        self.valor = valor
        self.maximo = maximo
def __int__(self):
    return int(self.valor)

def __float__(self):
    return float(self.valor)

def __bool__(self):
    return self.valor &gt; 0

def __lt__(self, other):
    if isinstance(other, (int, float)):
        return self.valor &lt; other
    return NotImplemented

p = Pontuacao(85) print(int(p)) # 85 print(float(p)) # 85.0 print(bool(p)) # True print(p > 50) # True

__slots__: Economia de Memória

O atributo especial __slots__ não é exatamente um método mágico, mas é um mecanismo poderoso para economizar memória. Ao declarar __slots__, você impede a criação do dicionário __dict__ em cada instância, reduzindo significativamente o consumo de memória para objetos que existem em grande número.

class Ponto:
    __slots__ = ("x", "y")
def __init__(self, x, y):
    self.x = x
    self.y = y

p = Ponto(10, 20) print(p.x, p.y) # 10 20

p.z = 30 # AttributeError: 'Ponto' object has no attribute 'z'

Em aplicações que criam milhões de objetos (como em processamento de dados ou jogos), o uso de __slots__ pode reduzir o uso de memória em até 50%, conforme documentado na seção sobre __slots__ da documentação oficial.

Data Classes vs Métodos Mágicos

Desde o Python 3.7, as dataclasses (documentadas na PEP 557) automatizam a implementação de vários métodos mágicos como __init__, __repr__, __eq__ e __hash__. Para classes simples que são principalmente contêineres de dados, as dataclasses são frequentemente a melhor escolha:

from dataclasses import dataclass

@dataclass class Produto: nome: str preco: float quantidade: int = 0

@property
def valor_total(self) -> float:
    return self.preco * self.quantidade

p1 = Produto("Notebook", 3500.00, 5) p2 = Produto("Notebook", 3500.00, 5) print(p1) # Produto(nome='Notebook', preco=3500.0, quantidade=5) print(p1 == p2) # True (eq automático)

Para casos mais complexos com lógica de negócio, validação ou comportamento específico, implementar manualmente os métodos mágicos ainda é a abordagem mais flexível.

Boas Práticas e Armadilhas Comuns

1. Sempre retorne NotImplemented em vez de lançar TypeError: Quando um método mágico não sabe lidar com o tipo recebido, retorne NotImplemented. Isso permite que o Python tente a operação inversa no outro operando.

2. Implemente __hash__ quando implementar __eq__: Objetos que implementam __eq__ sem __hash__ se tornam não-hasháveis e não podem ser usados em sets ou como chaves de dicionário.

3. Mantenha __repr__ não ambíguo e __str__ legível: A convenção é clara: __repr__ para o desenvolvedor, __str__ para o usuário final.

4. Cuidado com efeitos colaterais em __del__: O garbage collector do Python não garante quando __del__ será chamado. Use context managers para liberação determinística de recursos.

5. Prefira decoradores @property a __getattr__: Para acesso controlado a atributos específicos, @property é mais claro e previsível que __getattr__. Consulte a documentação da função property para mais detalhes.

6. Evite __slots__ prematuramente: Use __slots__ apenas quando você tem um número muito grande de instâncias e precisa otimizar o uso de memória. A otimização prematura adiciona complexidade desnecessária.

Conclusão

Os métodos mágicos são uma das características mais poderosas do Python. Eles permitem que você crie classes que se integram perfeitamente com a sintaxe e os protocolos da linguagem, resultando em código mais limpo, expressivo e idiomático.

Dominar __init__, __str__, __repr__, os métodos de comparação, operadores aritméticos, protocolo de container e context managers vai transformar a qualidade do seu código Python. Comece implementando estes métodos nas suas classes aos poucos — você verá a diferença na legibilidade e na flexibilidade do seu código.

Para continuar seus estudos em Python, confira nosso guia completo sobre Programação Orientada a Objetos em Python e também o tutorial de Decorators em Python, que complementam perfeitamente o que você aprendeu aqui sobre métodos mágicos.

E lembre-se: a documentação oficial é sua melhor amiga. Mantenha a página de Special Method Names na Python Documentation sempre à mão — ela é a referência definitiva sobre métodos mágicos em Python.