O web3.py é o cliente Python para nós Ethereum; a assinatura de mensagens e de transações fica a cargo do pacote eth-account (0.14.0 com web3 8.0.0), que por sua vez usa o eth-keys para a aritmética da curva. A maior parte do código de assinatura importa eth_account diretamente, como faz o exemplo.
Account.sign_message(encode_defunct(text=...)) implementa o personal_sign: o prefixo EIP-191 com o comprimento em bytes em decimal, keccak-256, um nonce RFC 6979 (conferido com um signatário de referência independente) e s na metade inferior, retornados como 65 bytes terminados em v = 27 ou 28. O encode_defunct é a parte que as pessoas esquecem: assinar bytes brutos com unsafe_sign_hash pula o prefixo, e é exatamente isso que torna uma assinatura inútil como prova de login em outros lugares.
Account.recover_message() recuperou o signatário a partir da assinatura high-S espelhada e também a partir de v = 0/1, então aceita mais formas do que o ethers — normalize o s você mesmo se as assinaturas forem armazenadas ou deduplicadas. A forma compacta EIP-2098 de 64 bytes foi rejeitada com Unexpected recoverable signature length: Expected 65, but got 64 bytes; expanda-a antes da recuperação.
Para dados tipados, use encode_typed_data (EIP-712) em vez de calcular você mesmo o hash do JSON, e confira no inspetor de EIP-712 o que a carteira está sendo solicitada a assinar. Carteiras de contrato inteligente exigem uma chamada ERC-1271 ao contrato em vez de recover_message; mensagens de login são mais fáceis de depurar no verificador de SIWE.
O exemplo assina uma mensagem de login e compara o endereço recuperado — a comparação que deveria substituir qualquer verificação byte a byte de assinaturas.