Python Criando suas Exceções
O Python já vem com dezenas de exceções: ValueError, KeyError, ZeroDivisionError… Elas falam dos problemas do Python: um texto que não vira número, uma chave que não existe, uma divisão por zero. Mas o seu programa tem problemas próprios. Na lojinha da Ana, "acabou o café" e "esse produto a gente não vende" são erros que nenhuma exceção do Python descreve direito. Nesta aula você vai criar as suas exceções, com nome, mensagem e até dados, para que o resto do programa saiba exatamente o que deu errado.
- Por que usar
ValueErrorpara tudo acaba misturando erros diferentes. - A criar uma exceção com
class NomeError(Exception):, e o costume doErrorno fim do nome. - A lançá-la com
raisee a capturá-la comexcept. - A montar uma família de exceções (uma mãe, várias filhas) e a capturar todas de uma vez.
- Por que a ordem dos
exceptimporta. - A guardar dados dentro da exceção (o produto, a quantidade) e a montar a mensagem com
super().__init__. - A trocar um erro do Python por um seu com
raise … from e. - Os erros reais:
exceptions must derive from BaseException, a mensagem que vira tupla, oexceptcom nome errado.
A ideia: alarmes com etiqueta
Imagine que a lojinha tivesse um alarme só, que toca por qualquer motivo: porta aberta, fumaça, freezer quente. Quando ele toca, a Ana tem de sair procurando o que foi. Agora imagine um painel com uma luz para cada problema: "freezer", "porta dos fundos", "fumaça". Ela olha e já sabe aonde ir. E ainda dá para ter uma luz geral, "problema na loja", que acende junto com qualquer uma das outras.
Cada exceção que você cria é uma dessas luzes com etiqueta. E a família de exceções (seção 4) é a luz geral.
Antes de começar, lembre: raise lança uma exceção (aula raise: Lançando Erros) e try/except a captura (aula try e except). Aqui você vai juntar as duas coisas com classes e herança.
1. O problema: um ValueError para tudo
A função vender recusa a venda quando não tem estoque. Sem exceção própria, a Ana usou o ValueError. A quantidade vem digitada como texto, então o programa também converte com int():
estoque = {"café": 2, "leite": 10}
def vender(produto, quantidade):
if quantidade > estoque[produto]:
raise ValueError(f"só temos {estoque[produto]} unidades de {produto}")
estoque[produto] -= quantidade
for texto in ["5", "dois"]:
try:
vender("café", int(texto))
except ValueError as e:
print("Não deu:", e)
$ python3 generico.py Não deu: só temos 2 unidades de café Não deu: invalid literal for int() with base 10: 'dois'
Os dois problemas caíram no mesmo except. Um é "falta café" (o cliente pode levar menos); o outro é "digitaram errado" (é preciso perguntar de novo). Eles pedem reações diferentes, mas o programa não tem como separá-los, porque o int("dois") também lança ValueError. A saída é dar ao problema do estoque uma etiqueta só dele.
2. A primeira exceção própria
class EstoqueInsuficienteError(Exception):
pass
estoque = {"café": 2, "leite": 10}
def vender(produto, quantidade):
if quantidade > estoque[produto]:
raise EstoqueInsuficienteError(f"só temos {estoque[produto]} unidades de {produto}")
estoque[produto] -= quantidade
vender("leite", 3)
print("Leite vendido. Sobraram", estoque["leite"])
vender("café", 5)
print("Esta linha não roda.")
- class EstoqueInsuficienteError
- Uma exceção é uma classe. O nome segue o costume das classes (cada palavra com maiúscula) e termina em
Error("erro"), como recomenda o guia de estilo do Python (aula PEP 8), para que quem lê saiba na hora que aquilo é um erro. - (Exception)
- A parte mais importante: ela herda de
Exception, a mãe de quase todas as exceções do Python. É isso que permite usá-la comraiseeexcept. - pass
- "Não faça nada." A classe não precisa de mais nada: herdou tudo o que uma exceção sabe fazer, inclusive guardar a mensagem.
- raise EstoqueInsuficienteError(f"…")
- Cria um objeto da exceção, com a mensagem entre parênteses, e o lança, como você faria com
raise ValueError("…").
$ python3 primeira.py
Leite vendido. Sobraram 7
Traceback (most recent call last):
File "/home/ana/primeira.py", line 16, in <module>
vender("café", 5)
~~~~~~^^^^^^^^^^^
File "/home/ana/primeira.py", line 10, in vender
raise EstoqueInsuficienteError(f"só temos {estoque[produto]} unidades de {produto}")
EstoqueInsuficienteError: só temos 2 unidades de café
O leite foi vendido normalmente. No café, a sua exceção apareceu num traceback igualzinho aos do Python: a linha 16 chamou vender, a linha 10 lançou o erro, e a última linha traz o nome da sua classe e a sua mensagem: EstoqueInsuficienteError: só temos 2 unidades de café. Como ninguém capturou, o programa parou, e o último print não rodou.
3. Capturando a sua exceção
Agora cada problema tem o seu except:
class EstoqueInsuficienteError(Exception):
pass
estoque = {"café": 2, "leite": 10}
def vender(produto, quantidade):
if quantidade > estoque[produto]:
raise EstoqueInsuficienteError(f"só temos {estoque[produto]} unidades de {produto}")
estoque[produto] -= quantidade
for texto in ["5", "dois", "1"]:
try:
vender("café", int(texto))
print(f"Vendi {texto} café. Sobraram {estoque['café']}.")
except EstoqueInsuficienteError as e:
print("Falta produto:", e)
except ValueError:
print(f"Quantidade inválida: {texto!r}")
$ python3 capturar.py Falta produto: só temos 2 unidades de café Quantidade inválida: 'dois' Vendi 1 café. Sobraram 1.
Três pedidos, três destinos: o "5" caiu no except EstoqueInsuficienteError, o "dois" caiu no except ValueError (lançado pelo int()), e o "1" foi vendido. O as e guarda o objeto da exceção, e print(e) mostra a mensagem que você passou no raise.
Missão: o programa lança e captura a EstoqueInsuficienteError, mas esqueceram de criar a classe (rode para ver o NameError). Crie a exceção no começo do arquivo.
estoque = {"café": 2}
def vender(produto, quantidade):
if quantidade > estoque[produto]:
raise EstoqueInsuficienteError(f"só temos {estoque[produto]} unidades de {produto}")
estoque[produto] -= quantidade
try:
vender("café", 5)
except EstoqueInsuficienteError as e:
print("Falta produto:", e)
class EstoqueInsuficienteError(Exception):
pass
estoque = {"café": 2}
def vender(produto, quantidade):
if quantidade > estoque[produto]:
raise EstoqueInsuficienteError(f"só temos {estoque[produto]} unidades de {produto}")
estoque[produto] -= quantidade
try:
vender("café", 5)
except EstoqueInsuficienteError as e:
print("Falta produto:", e)
4. Uma família de exceções
A lojinha tem mais de um problema próprio: além do estoque, alguém pode pedir um produto que a loja não vende. A Ana cria uma exceção mãe, LojaError, e as outras herdam dela:
class LojaError(Exception):
"""Erro de qualquer regra da lojinha."""
class EstoqueInsuficienteError(LojaError):
"""Pediram mais do que tem no estoque."""
class ProdutoNaoEncontradoError(LojaError):
"""O produto não existe no cadastro."""
estoque = {"café": 2, "leite": 10}
def vender(produto, quantidade):
if produto not in estoque:
raise ProdutoNaoEncontradoError(f"não vendemos {produto}")
if quantidade > estoque[produto]:
raise EstoqueInsuficienteError(f"só temos {estoque[produto]} unidades de {produto}")
estoque[produto] -= quantidade
for produto, quantidade in [("café", 5), ("pizza", 1), ("leite", 1)]:
try:
vender(produto, quantidade)
print(f"Vendido: {quantidade} {produto}")
except LojaError as e:
print(f"{type(e).__name__}: {e}")
print(issubclass(EstoqueInsuficienteError, LojaError))
print(issubclass(LojaError, Exception))
- class LojaError(Exception):
- A mãe da família. Herda de
Exception. - """Erro de qualquer regra da lojinha."""
- Uma docstring (aula Docstrings) explicando para que serve a exceção. Ela já basta como corpo da classe, no lugar do
pass. - class EstoqueInsuficienteError(LojaError):
- Uma filha: herda de
LojaError(e, por tabela, deException). - except LojaError as e:
- Captura a
LojaErrore todas as filhas dela. - type(e).__name__
- O nome da classe da exceção que chegou, para mostrar qual foi.
- issubclass(A, B)
- "A é filha (ou neta…) de B?"
$ python3 hierarquia.py EstoqueInsuficienteError: só temos 2 unidades de café ProdutoNaoEncontradoError: não vendemos pizza Vendido: 1 leite True True
Um except só pegou os dois tipos de problema. É a luz geral do painel: quem chama vender e quer tratar "qualquer problema da loja" escreve except LojaError; quem quer tratar só a falta de estoque escreve except EstoqueInsuficienteError. E nenhum dos dois engole por engano um ValueError ou um KeyError que não tem nada a ver com as regras da loja.
O Python organiza as exceções dele do mesmo jeito. Por exemplo, KeyError e IndexError são filhas de LookupError ("erro de busca"), e as duas são netas de Exception:
Exception
├── LookupError
│ ├── KeyError
│ └── IndexError
├── ValueError
└── LojaError
├── EstoqueInsuficienteError
└── ProdutoNaoEncontradoError
Missão: o except LojaError deveria pegar os dois erros da loja, mas o programa para com um traceback. O problema está na família: faça as duas exceções filhas herdarem de LojaError.
class LojaError(Exception):
pass
class EstoqueInsuficienteError(Exception):
pass
class ProdutoNaoEncontradoError(Exception):
pass
estoque = {"café": 2, "leite": 10}
def vender(produto, quantidade):
if produto not in estoque:
raise ProdutoNaoEncontradoError(f"não vendemos {produto}")
if quantidade > estoque[produto]:
raise EstoqueInsuficienteError(f"só temos {estoque[produto]} unidades de {produto}")
estoque[produto] -= quantidade
for produto, quantidade in [("café", 5), ("pizza", 1), ("leite", 1)]:
try:
vender(produto, quantidade)
print(f"Vendido: {quantidade} {produto}")
except LojaError as e:
print(f"{type(e).__name__}: {e}")
class LojaError(Exception):
pass
class EstoqueInsuficienteError(LojaError):
pass
class ProdutoNaoEncontradoError(LojaError):
pass
estoque = {"café": 2, "leite": 10}
def vender(produto, quantidade):
if produto not in estoque:
raise ProdutoNaoEncontradoError(f"não vendemos {produto}")
if quantidade > estoque[produto]:
raise EstoqueInsuficienteError(f"só temos {estoque[produto]} unidades de {produto}")
estoque[produto] -= quantidade
for produto, quantidade in [("café", 5), ("pizza", 1), ("leite", 1)]:
try:
vender(produto, quantidade)
print(f"Vendido: {quantidade} {produto}")
except LojaError as e:
print(f"{type(e).__name__}: {e}")
5. A ordem dos except: da filha para a mãe
Quando há vários except, o Python testa um por um, de cima para baixo, e usa o primeiro que servir. Como a mãe serve para todas as filhas, colocá-la primeiro esconde as outras:
class LojaError(Exception):
pass
class EstoqueInsuficienteError(LojaError):
pass
try:
raise EstoqueInsuficienteError("só temos 2 unidades de café")
except LojaError as e:
print("Erro da loja:", e)
except EstoqueInsuficienteError as e:
print("Falta produto:", e)
$ python3 ordem_except.py Erro da loja: só temos 2 unidades de café
O erro era de estoque, mas quem tratou foi o except LojaError. O segundo except nunca vai rodar, e o Python não avisa. Regra: escreva os except do mais específico (a filha) para o mais geral (a mãe).
6. Exceções que carregam dados
Uma mensagem em texto é boa para mostrar, mas ruim para fazer contas. Se o programa quiser saber quantas unidades faltaram, ele teria de ler a frase. Melhor guardar os dados na própria exceção, como atributos:
class EstoqueInsuficienteError(Exception):
def __init__(self, produto, pedido, disponivel):
self.produto = produto
self.pedido = pedido
self.disponivel = disponivel
super().__init__(f"pediram {pedido} {produto}, mas só temos {disponivel}")
estoque = {"café": 2}
def vender(produto, quantidade):
if quantidade > estoque[produto]:
raise EstoqueInsuficienteError(produto, quantidade, estoque[produto])
estoque[produto] -= quantidade
try:
vender("café", 5)
except EstoqueInsuficienteError as e:
print("Mensagem:", e)
print("Faltam", e.pedido - e.disponivel, "unidades de", e.produto)
print(e.args)
vender("café", 7)
- def __init__(self, produto, pedido, disponivel):
- A exceção agora recebe três dados, em vez de uma mensagem pronta (aula __init__ e self).
- self.produto = produto …
- Guarda cada dado num atributo, como em qualquer classe.
- super().__init__(f"…")
- Chama o
__init__da mãe,Exception, com a mensagem montada a partir dos dados. É essa mensagem que oprint(e)e o traceback mostram. Osuper()é da aula Herança. - raise EstoqueInsuficienteError(produto, quantidade, estoque[produto])
- Quem lança passa os dados; a exceção monta a frase sozinha. Assim a mensagem sai sempre no mesmo formato.
- e.pedido - e.disponivel
- Quem captura usa os dados direto, sem ler a frase.
- e.args
- Uma tupla com o que foi passado ao
__init__daException: aqui, só a mensagem.
$ python3 atributos.py
Mensagem: pediram 5 café, mas só temos 2
Faltam 3 unidades de café
('pediram 5 café, mas só temos 2',)
Traceback (most recent call last):
File "/home/ana/atributos.py", line 25, in <module>
vender("café", 7)
~~~~~~^^^^^^^^^^^
File "/home/ana/atributos.py", line 14, in vender
raise EstoqueInsuficienteError(produto, quantidade, estoque[produto])
EstoqueInsuficienteError: pediram 7 café, mas só temos 2
Dentro do try, o programa mostrou a mensagem e fez a conta com os atributos. Na última linha, sem try, o traceback mostrou a mensagem montada pelo super().__init__. Esquecer essa linha tem um efeito estranho, que você vai ver em Quando dá errado.
7. Trocando um erro do Python por um seu: raise … from e
A função preco_de procura o preço num dicionário. Se o produto não existe, o dicionário lança KeyError, mas para quem usa a lojinha seria melhor receber um ProdutoNaoEncontradoError. Dá para capturar um e lançar o outro, deixando registrado que um causou o outro:
class ProdutoNaoEncontradoError(Exception):
pass
precos = {"café": 18.9, "leite": 5.49}
def preco_de(produto):
try:
return precos[produto]
except KeyError as e:
raise ProdutoNaoEncontradoError(f"não vendemos {produto}") from e
print(preco_de("café"))
print(preco_de("pizza"))
$ python3 from_e.py
18.9
Traceback (most recent call last):
File "/home/ana/from_e.py", line 10, in preco_de
return precos[produto]
~~~~~~^^^^^^^^^
KeyError: 'pizza'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/ana/from_e.py", line 16, in <module>
print(preco_de("pizza"))
~~~~~~~~^^^^^^^^^
File "/home/ana/from_e.py", line 12, in preco_de
raise ProdutoNaoEncontradoError(f"não vendemos {produto}") from e
ProdutoNaoEncontradoError: não vendemos pizza
Agora são dois tracebacks, ligados por uma frase:
- O primeiro é o erro original:
KeyError: 'pizza', na linha 10. The above exception was the direct cause of the following exception: "a exceção acima foi a causa direta da exceção abaixo". É ofrom eque cria essa ligação.- O segundo é o seu erro,
ProdutoNaoEncontradoError: não vendemos pizza, lançado na linha 12. É ele que umexceptde fora vai receber.
Sem o from e, o Python também mostra os dois, mas com outra frase: During handling of the above exception, another exception occurred ("durante o tratamento da exceção acima, aconteceu outra"). Ela dá a entender que o seu except é que falhou, quando na verdade você trocou o erro de propósito. O from e deixa clara a intenção.
Formas diferentes de fazer a mesma coisa
| Situação | Use |
|---|---|
| O problema já tem uma exceção do Python que descreve bem (um argumento com valor inválido) | A do Python: raise ValueError("…") |
| Um problema das regras do seu programa (estoque, saldo, cadastro) | Uma exceção própria: class EstoqueInsuficienteError(Exception): pass |
| Vários problemas do mesmo assunto, que às vezes são tratados juntos | Uma mãe (LojaError) e filhas |
| Quem captura precisa de números ou nomes, não só de uma frase | Atributos no __init__ + super().__init__(mensagem) |
| Um erro do Python que você quer apresentar como erro seu | raise SeuError("…") from e |
Uma exceção própria também pode herdar de uma exceção do Python, em vez de Exception: class PrecoInvalidoError(ValueError). Assim ela é capturada tanto por except PrecoInvalidoError quanto por um except ValueError que já existia no programa.
Quando dá errado
Exception
class EstoqueInsuficienteError:
pass
raise EstoqueInsuficienteError()
$ python3 sem_heranca.py
Traceback (most recent call last):
File "/home/ana/sem_heranca.py", line 5, in <module>
raise EstoqueInsuficienteError()
TypeError: exceptions must derive from BaseException
"Exceções precisam derivar de BaseException." Sem o (Exception), é uma classe comum, e o raise só aceita objetos da família das exceções. (O BaseException é a avó de todas: a mãe da Exception.) Se você tentar passar uma mensagem, raise EstoqueInsuficienteError("…"), o erro chega antes, ao criar o objeto: TypeError: EstoqueInsuficienteError() takes no arguments, porque uma classe comum sem __init__ não aceita argumentos. Como consertar: class EstoqueInsuficienteError(Exception):.
class EstoqueInsuficienteError(Exception):
def __init__(self, produto, pedido, disponivel):
self.produto = produto
self.pedido = pedido
self.disponivel = disponivel
try:
raise EstoqueInsuficienteError("café", 5, 2)
except EstoqueInsuficienteError as e:
print("Mensagem:", e)
$ python3 sem_super.py
Mensagem: ('café', 5, 2)
Sem erro, mas a mensagem virou ('café', 5, 2). Faltou o super().__init__(mensagem): a Exception não recebeu frase nenhuma e, para não ficar calada, mostra os argumentos que a exceção recebeu, numa tupla. Como consertar: no fim do __init__, super().__init__(f"pediram {pedido} {produto}, mas só temos {disponivel}").
BaseException: o except Exception não pega
class EstoqueInsuficienteError(BaseException):
pass
try:
raise EstoqueInsuficienteError("só temos 2 unidades de café")
except Exception as e:
print("Peguei:", e)
print("Fim")
$ python3 base_exception.py
Traceback (most recent call last):
File "/home/ana/base_exception.py", line 6, in <module>
raise EstoqueInsuficienteError("só temos 2 unidades de café")
EstoqueInsuficienteError: só temos 2 unidades de café
O except Exception não pegou, e o programa parou sem chegar no Fim. A BaseException está acima da Exception: ela é reservada para casos especiais, como o Ctrl+C (KeyboardInterrupt) e o sys.exit() (SystemExit), que de propósito escapam de um except Exception. Como consertar: as suas exceções herdam sempre de Exception (ou de uma filha dela).
except: o erro só aparece quando a exceção chega
class EstoqueInsuficienteError(Exception):
pass
try:
print("Vendendo...")
except EstoqueInsuficienteErro:
print("Falta produto")
print("Sem erro, o except nem foi lido.")
try:
raise EstoqueInsuficienteError("só temos 2 unidades de café")
except EstoqueInsuficienteErro:
print("Falta produto")
$ python3 nome_errado_except.py
Vendendo...
Sem erro, o except nem foi lido.
Traceback (most recent call last):
File "/home/ana/nome_errado_except.py", line 12, in <module>
raise EstoqueInsuficienteError("só temos 2 unidades de café")
EstoqueInsuficienteError: só temos 2 unidades de café
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/ana/nome_errado_except.py", line 13, in <module>
except EstoqueInsuficienteErro:
^^^^^^^^^^^^^^^^^^^^^^^
NameError: name 'EstoqueInsuficienteErro' is not defined. Did you mean: 'EstoqueInsuficienteError'?
Nos dois except, falta o r final de Error. No primeiro try, nada deu errado, e o Python nem leu o nome do except: ele só olha o que está no except quando uma exceção chega. No segundo, a exceção chegou, o Python foi conferir o nome, e aí veio o NameError, com a frase "durante o tratamento da exceção acima, aconteceu outra" e a sugestão do nome certo. É um defeito traiçoeiro: o programa pode rodar por semanas até o dia em que o erro acontece de verdade. Como consertar: confira o nome (o "Did you mean" ajuda) e teste o caminho do erro, provocando a exceção de propósito.
Você já viu na seção 5: com except LojaError antes de except EstoqueInsuficienteError, o segundo nunca roda, sem aviso nenhum. Como consertar: do mais específico para o mais geral.
Referência rápida
| Escrita | O que faz |
|---|---|
class NomeError(Exception): pass | Cria uma exceção própria |
class FilhaError(MaeError): pass | Cria uma exceção filha (capturada também por except MaeError) |
raise NomeError("mensagem") | Lança a exceção com uma mensagem |
except NomeError as e: | Captura a exceção (e as filhas dela) |
print(e) / str(e) | A mensagem |
e.args | A tupla do que foi passado à Exception |
type(e).__name__ | O nome da classe da exceção |
def __init__(self, …): … super().__init__(mensagem) | Exceção com atributos e mensagem montada |
raise NomeError("…") from e | Troca uma exceção por outra, registrando a causa |
issubclass(Filha, Mae) | Confere a família |
| Sintoma | Causa provável |
|---|---|
TypeError: exceptions must derive from BaseException | A classe não herda de Exception |
TypeError: NomeError() takes no arguments no raise | Classe comum (sem herança) recebendo mensagem |
A mensagem aparece como ('café', 5, 2) | __init__ próprio sem super().__init__(mensagem) |
O except Exception não pega a sua exceção | Ela herda de BaseException |
Um except nunca roda, sem aviso | Um except da mãe está antes dele |
NameError no except, só quando o erro acontece | Nome da exceção digitado errado no except |
The above exception was the direct cause… | Não é defeito: foi um raise … from e mostrando a causa |
Exercícios
1. A mãe certa
Complete para criar a exceção da lojinha:
pass
2. Lançando
Complete a palavra que lança a exceção:
3. EstoqueInsuficienteError e ProdutoNaoEncontradoError herdam de LojaError. Qual except pega as duas?
4. Por que o except LojaError deve vir depois do except EstoqueInsuficienteError?
5. A sua exceção tem um __init__ próprio, e print(e) mostra ('café', 5, 2). O que faltou?
6. Qual é o costume para o nome de uma exceção?
Missão final: o saque sem saldo
Missão: a conta tem R$ 50 e a Ana tentou sacar R$ 80. A exceção SaldoInsuficienteError recebe o saldo e o valor, mas não guarda nada (rode: a mensagem sai como tupla e o e.valor dá erro). Escreva o __init__ dela: guarde saldo e valor em atributos e monte uma mensagem com super().__init__(…).
class SaldoInsuficienteError(Exception):
pass # troque: guarde o saldo e o valor, e monte a mensagem
def sacar(saldo, valor):
if valor > saldo:
raise SaldoInsuficienteError(saldo, valor)
return saldo - valor
try:
sacar(50, 80)
except SaldoInsuficienteError as e:
print("Não deu:", e)
print(f"Faltam R$ {e.valor - e.saldo:.2f}")
class SaldoInsuficienteError(Exception):
def __init__(self, saldo, valor):
self.saldo = saldo
self.valor = valor
super().__init__(f"saque de R$ {valor:.2f}, mas o saldo é R$ {saldo:.2f}")
def sacar(saldo, valor):
if valor > saldo:
raise SaldoInsuficienteError(saldo, valor)
return saldo - valor
try:
sacar(50, 80)
except SaldoInsuficienteError as e:
print("Não deu:", e)
print(f"Faltam R$ {e.valor - e.saldo:.2f}")
Com a solução, o lab mostrou:
$ python3 main.py Não deu: saque de R$ 80.00, mas o saldo é R$ 50.00 Faltam R$ 30.00
Resumo
- Uma exceção própria é uma classe que herda de
Exception:class EstoqueInsuficienteError(Exception): pass. O nome termina emError. - Ela é lançada com
raisee capturada comexcept, como as do Python, e separa os problemas do seu programa dos problemas do Python. - Uma exceção mãe (
LojaError) com filhas permite tratar todas juntas ou cada uma separada. Nosexcept, a filha vem antes da mãe. - Para carregar dados, escreva um
__init__com atributos e chamesuper().__init__(mensagem); sem isso, a mensagem vira uma tupla. raise SeuError(…) from etroca um erro do Python pelo seu e registra a causa ("direct cause").- Nunca herde de
BaseException: umexcept Exceptionnão pegaria a sua exceção.