cryptography is what most Python code — PyJWT, Authlib, paramiko, certbot, the AWS and Google SDKs — uses for real cryptography. Its hazmat layer exposes keys and signatures directly; the wheels bundle their own OpenSSL, so behaviour follows the package version rather than the operating system (50.0.2 ships OpenSSL 4.0.3).
ECDSA signatures come out as ASN.1 DER, and verify() expects DER. WebCrypto, JWS and most hardware tokens use the 64-byte raw r‖s form, which verify() rejects with the same InvalidSignature as a forged signature. decode_dss_signature and encode_dss_signature convert between DER and the integer pair; the fixed-width padding is yours to add, exactly as the sample shows, or use the DER ⇄ raw converter to check by hand.
The nonce is random unless you ask for RFC 6979: ec.ECDSA(hashes.SHA256(), deterministic_signing=True) reproduced the RFC's P-256 test vector exactly — including its high s, because nothing in the package normalizes s, and verification accepts both halves. Signatures headed for a low-S verifier such as libsecp256k1 or @noble/curves need s replaced by n − s first. sign() and verify() hash the data themselves; pass Prehashed(hashes.SHA256()) when you already hold a digest, or you will hash twice.
Curve coverage is broad — P-224 through P-521, secp256k1 and the Brainpool r1 curves — plus Ed25519, Ed448, ML-KEM-768/1024 and ML-DSA-44/65/87 in 50.0.2. Keys load from PEM and DER (SPKI, PKCS#8, encrypted PKCS#8, SEC1), OpenSSH and bare SEC1 points; there is no JWK reader, so JWKs go through PyJWT, Authlib or jwcrypto, or the JWK converter.
Most errors people search for come from key loading rather than signing: a public key passed to load_pem_private_key, an encrypted key without a password, or a point that is not on the named curve. The signature verifier and the library interop matrix help when a signature from another stack refuses to verify.