web3.py is the Python client for Ethereum nodes; message and transaction signing are delegated to the eth-account package (0.14.0 with web3 8.0.0), which in turn uses eth-keys for the curve arithmetic. Most signing code imports eth_account directly, as the sample does.
Account.sign_message(encode_defunct(text=...)) implements personal_sign: the EIP-191 prefix with the decimal byte length, keccak-256, an RFC 6979 nonce (matched against an independent reference signer) and s in the low half, returned as 65 bytes ending in v = 27 or 28. encode_defunct is the piece people forget: signing raw bytes with unsafe_sign_hash skips the prefix, which is exactly what makes a signature unusable as a login proof elsewhere.
Account.recover_message() recovered the signer from the mirrored high-S signature as well as from v = 0/1, so it accepts more forms than ethers does — normalize s yourself if signatures are stored or deduplicated. The 64-byte EIP-2098 compact form was rejected with Unexpected recoverable signature length: Expected 65, but got 64 bytes; expand it before recovery.
For typed data use encode_typed_data (EIP-712) rather than hashing the JSON yourself, and check what a wallet is being asked to sign with the EIP-712 inspector. Smart-contract wallets need an ERC-1271 call to the contract instead of recover_message; sign-in messages are easiest to debug in the SIWE verifier.
The sample signs a sign-in message and compares the recovered address — the comparison that should replace any byte-for-byte check of signatures.