coincurve wraps libsecp256k1 with CFFI and ships prebuilt wheels, so Python code gets the same constant-time secp256k1 implementation Bitcoin Core uses without a compiler. Version 21.0.0 publishes wheels for CPython 3.10 to 3.13 only; on Python 3.14 pip falls back to a source build, which fails in slim images without a C toolchain — that is why the check ran on Python 3.12.
The defaults suit Bitcoin rather than Ethereum. sign() hashes the message with SHA-256, uses an RFC 6979 nonce (the check matched an independent reference signer) and returns a DER signature with s already in the low half. Ethereum, Nostr and most wallets hash with keccak-256 or sign a 32-byte digest, so pass a hasher or hasher=None; leaving the default in place produces valid-looking signatures over the wrong hash.
Verification inherits libsecp256k1's strictness. verify() returned False for the mirrored high-S form of a valid signature — a signature from OpenSSL, Java or a cloud KMS fails about half the time unless s is normalized first. Input that is not DER raises ValueError: The DER-encoded signature could not be parsed. instead of returning False; raw 64-byte signatures need verify() on a DER re-encoding, and the DER ⇄ raw converter shows both forms.
Beyond plain ECDSA, sign_recoverable() returns the 65-byte r ‖ s ‖ recovery-id form and PublicKey.from_signature_and_message() recovers the key, which is how Ethereum addresses are derived from signatures; sign_schnorr() produces BIP-340 signatures for Taproot and Nostr. Public keys are SEC1 points, compressed by default; private keys are raw 32-byte scalars, and to_pem()/to_der() write PKCS#8.
The sample shows both modes: the SHA-256 default, and a keccak digest signed recoverably as Ethereum expects. Check results against the Bitcoin message verifier or the Ethereum signature verifier.