Post-quantum cryptography is moving from research into real software stacks.

That matters for Python developers because cryptographic libraries, TLS implementations, APIs, certificates, and application-level signatures will eventually need to deal with algorithms designed to resist attacks from large-scale quantum computers.

One of the important algorithms in this area is ML-DSA, the post-quantum digital signature algorithm standardized in FIPS 204.

Python 3.15 is interesting here because its SSL layer adds better controls for TLS signature algorithms and key-exchange groups, including support for post-quantum groups when the underlying OpenSSL version provides them. At the same time, application-level ML-DSA signing can be tested through libraries such as cryptography, which provides MLDSA44, MLDSA65, and MLDSA87 key types.

There is an important distinction, though.

Python 3.15 does not mean that this works:

Python
import ssl

ssl.mldsa_sign(...)

There is no such standard-library API.

Instead, a Python application can have two separate post-quantum concerns:

Code
TLS communication
       ↓
Python ssl + OpenSSL

Application signatures
       ↓
cryptography + OpenSSL

Understanding that separation is important before testing ML-DSA in a real application.

What Is ML-DSA?

ML-DSA is a post-quantum digital signature algorithm based on module lattices.

It is standardized as part of FIPS 204 and is intended to provide digital signatures that remain secure against attackers with sufficiently powerful quantum computers.

Digital signatures are commonly used for:

A simplified signature workflow looks like this:

Code
Private key
    ↓
Sign message
    ↓
Signature
    ↓
Message + Signature
    ↓
Public key
    ↓
Verify

ML-DSA follows the same high-level idea.

The difference is the underlying cryptographic construction.

ML-DSA Has Three Parameter Sets

ML-DSA defines three main parameter sets:

Code
ML-DSA-44
ML-DSA-65
ML-DSA-87

OpenSSL 3.5 implements all three. They correspond to different security categories.

A simplified comparison is:

Algorithm

Security Category

Signature Size

ML-DSA-44

2

~2.4 KB

ML-DSA-65

3

~3.3 KB

ML-DSA-87

5

~4.6 KB

The exact sizes depend on the format and implementation details, but the important point is that ML-DSA signatures are substantially larger than many traditional signatures.

That can matter for APIs, tokens, certificates, network traffic, and storage.

Why Python Developers Should Care

Suppose an application currently uses:

Code
RSA
ECDSA
Ed25519

for digital signatures.

Moving to post-quantum signatures is not simply a matter of changing:

Python
algorithm = "RSA"

to:

Python
algorithm = "ML-DSA"

You need to think about:

The Python application is only one part of the system.

Python 3.15 and OpenSSL

Python's ssl module is built around OpenSSL on supported CPython builds.

Python 3.15 expands the SSL API with methods such as:

Python
SSLContext.set_groups()
SSLContext.get_groups()
SSLSocket.group()

These APIs allow applications to inspect and configure the groups used during TLS key agreement.

Python's documentation notes that these groups can include post-quantum groups when the underlying OpenSSL version supports them. Some of the APIs require OpenSSL 3.2 or later, while get_groups() requires OpenSSL 3.5 or later.

Python 3.15 also adds APIs for controlling TLS signature algorithms:

Python
ssl.get_sigalgs()
SSLContext.set_client_sigalgs()
SSLContext.set_server_sigalgs()
SSLSocket.client_sigalg()
SSLSocket.server_sigalg()

These APIs make it easier to inspect and control which signature algorithms can be used during TLS authentication.

This is useful for post-quantum migration testing.

Check Your OpenSSL Version First

Before testing anything, check the OpenSSL version used by Python.

Run:

Python
import ssl

print(ssl.OPENSSL_VERSION)

You can also inspect:

Python
import ssl

print(ssl.OPENSSL_VERSION_INFO)

For command-line testing:

Bash
openssl version

This matters because Python's capabilities depend partly on the OpenSSL library available to the interpreter.

For example, OpenSSL 3.5 provides native ML-DSA key generation and signing support.

A Python installation linked against a different OpenSSL version may not expose the same capabilities.

Testing ML-DSA at the Application Level

For application-level signatures, the cryptography package currently provides ML-DSA support.

For example:

Python
from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA65PrivateKey

private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()

message = b"Hello from Python"

signature = private_key.sign(message)

public_key.verify(signature, message)

print("Signature verified")

This is a normal sign-and-verify workflow.

The private key creates the signature.

The public key verifies it.

The verification call raises an exception if the signature is invalid.

Why ML-DSA-65 Is a Useful Test Starting Point

If you are experimenting with the three parameter sets, ML-DSA-65 is a reasonable starting point because it sits between ML-DSA-44 and ML-DSA-87.

For example:

Python
from cryptography.hazmat.primitives.asymmetric.mldsa import (
    MLDSA65PrivateKey
)

private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()

message = b"production test message"

signature = private_key.sign(message)

print("Signature bytes:", len(signature))

public_key.verify(signature, message)

print("Verification successful")

This gives you two useful measurements:

Code
Signature generation
Signature size

You should also measure verification time.

Test All Three Parameter Sets

If your application needs to choose an ML-DSA security level, test all three.

For example:

Python
from cryptography.hazmat.primitives.asymmetric.mldsa import (
    MLDSA44PrivateKey,
    MLDSA65PrivateKey,
    MLDSA87PrivateKey,
)

for key_type in (
    MLDSA44PrivateKey,
    MLDSA65PrivateKey,
    MLDSA87PrivateKey,
):
    private_key = key_type.generate()
    public_key = private_key.public_key()

    message = b"test message"
    signature = private_key.sign(message)

    public_key.verify(signature, message)

    print(
        key_type.__name__,
        len(signature)
    )

This is a much better starting point than choosing an algorithm solely because its name sounds stronger.

Measure the behavior that matters to your application.

Signature Size Matters

One of the biggest practical differences with post-quantum signatures is size.

A traditional Ed25519 signature is only 64 bytes.

ML-DSA signatures are much larger.

For example, current cryptography documentation lists approximately:

Code
ML-DSA-44: 2420 bytes
ML-DSA-65: 3309 bytes
ML-DSA-87: 4627 bytes

for the signature sizes.

That difference becomes important if signatures are included in:

Code
JWT-like tokens
HTTP headers
API requests
Database records
Messages
Certificates
Signed URLs
Software packages

A design that works comfortably with a 64-byte signature may behave very differently with a signature several kilobytes long.

Benchmark Signing and Verification

Do not benchmark just one operation.

Measure:

Code
Key generation
Signing
Verification
Serialization
Deserialization

For example:

Python
import time

from cryptography.hazmat.primitives.asymmetric.mldsa import (
    MLDSA65PrivateKey
)

private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()

message = b"benchmark message"

start = time.perf_counter()

for _ in range(1000):
    signature = private_key.sign(message)

sign_time = time.perf_counter() - start

start = time.perf_counter()

for _ in range(1000):
    public_key.verify(signature, message)

verify_time = time.perf_counter() - start

print(f"Signing: {sign_time:.4f}s")
print(f"Verification: {verify_time:.4f}s")

This is only a basic benchmark.

For production decisions, run multiple iterations and test realistic message sizes.

Test Different Message Sizes

A signature system should not be benchmarked using only:

Python
message = b"hello"

Test several sizes:

Code
1 KB
10 KB
100 KB
1 MB
10 MB

For ML-DSA, the signature operation internally handles the message according to the algorithm's defined encoding rules. OpenSSL also exposes context and message-encoding controls.

For large application data, you should also consider whether signing the entire payload is actually necessary.

A common architecture is:

Code
Large document
      ↓
Hash
      ↓
Sign digest or protocol-defined representation

But do not invent your own ML-DSA pre-hashing scheme. Follow the API and protocol specification you are implementing.

Context Strings

ML-DSA supports an optional context string.

For example:

Python
from cryptography.hazmat.primitives.asymmetric.mldsa import (
    MLDSA65PrivateKey
)

private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()

message = b"payment request"
context = b"payment-api-v1"

signature = private_key.sign(
    message,
    context
)

public_key.verify(
    signature,
    message,
    context
)

The same context must be used during verification.

The current cryptography API limits the context string to 255 bytes.

Context strings are useful when the same key is used for different protocol purposes.

For example:

Code
payment-api-v1
document-signing-v1
software-update-v1

This can help prevent a signature created for one purpose from being accidentally accepted in another context.

The exact context design should come from the protocol being implemented.

What Happens If the Message Changes?

A digital signature should fail verification if the signed message is changed.

For example:

Python
from cryptography.hazmat.primitives.asymmetric.mldsa import (
    MLDSA65PrivateKey
)

private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()

message = b"amount=100"
signature = private_key.sign(message)

public_key.verify(
    signature,
    b"amount=100"
)

print("Valid")

But:

Python
public_key.verify(
    signature,
    b"amount=1000"
)

should fail.

This is one of the first negative tests you should add to an application.

Test Corrupted Signatures

Do not test only successful verification.

Test failure cases too.

For example:

Python
from cryptography.exceptions import InvalidSignature

try:
    public_key.verify(
        signature,
        b"tampered message"
    )
except InvalidSignature:
    print("Tampering detected")

Also test:

Code
Modified signature
Wrong public key
Wrong context
Modified message
Truncated signature
Empty message
Large message

These tests are more useful than simply proving that a valid signature works.

Test Key Serialization

Applications rarely keep a private key as a Python object forever.

Keys usually need to be stored or transferred using a standard encoding.

The cryptography package supports key serialization, including PEM and DER representations for supported key types.

A production test should therefore include:

Code
Generate key
   ↓
Serialize key
   ↓
Store / load
   ↓
Deserialize
   ↓
Sign
   ↓
Verify

Do not assume that because signing works in memory, your complete key-management workflow works.

OpenSSL Can Test ML-DSA Independently

It is useful to test ML-DSA outside Python as well.

OpenSSL 3.5 provides ML-DSA key generation:

Bash
openssl genpkey -algorithm ML-DSA-65 -out mldsa65.pem

You can then sign:

Bash
openssl pkeyutl \
    -sign \
    -in message.txt \
    -inkey mldsa65.pem \
    -out signature.bin

And verify:

Bash
openssl pkeyutl \
    -verify \
    -in message.txt \
    -inkey mldsa65.pem \
    -sigfile signature.bin

OpenSSL documents ML-DSA-44, ML-DSA-65, and ML-DSA-87 support through its EVP interfaces and pkeyutl command.

This gives you a useful interoperability test:

Code
Python
  ↓
Generate/sign
  ↓
OpenSSL
  ↓
Verify

and then:

OpenSSL
  ↓
Generate/sign
  ↓
Python
  ↓
Verify

For a real migration, cross-implementation testing is extremely valuable.

Testing TLS Is a Separate Problem

Application-level ML-DSA signatures and TLS authentication are related, but they are not the same thing.

For application signing:

Code
Python application
       ↓
ML-DSA
       ↓
Signature

For TLS:

Code
Python application
       ↓
ssl
       ↓
OpenSSL
       ↓
TLS handshake
       ↓
Negotiated groups / signature algorithms

Python 3.15 adds APIs that help inspect and control the TLS negotiation.

For example:

Python
import ssl

print(ssl.get_sigalgs())

The available results depend on the OpenSSL version and enabled providers.

Inspect TLS Groups

Python 3.15 adds:

Python
context.get_groups()

when supported by the underlying OpenSSL version.

For example:

Python
import ssl

context = ssl.create_default_context()

print(context.get_groups())

If the OpenSSL version does not provide the required functionality, the method may not be available in the expected form.

The key point is that Python is exposing the TLS controls while OpenSSL supplies the cryptographic implementation.

Python's documentation specifically notes OpenSSL version requirements for these APIs.

Configure TLS Groups Carefully

Python 3.15 provides:

Python
context.set_groups(...)

This allows an application to control the groups used for TLS key agreement.

For example:

Python
import ssl

context = ssl.create_default_context()

context.set_groups([
    "X25519",
    "secp256r1",
])

The exact group names available depend on the OpenSSL version and configuration.

For post-quantum testing, do not copy a group name from one environment and assume it exists everywhere.

First inspect the available groups:

Python
print(context.get_groups())

Then select a configuration supported by both endpoints.

TLS Signature Algorithms Are Different From TLS Groups

This distinction is easy to miss.

A TLS group is related to key agreement.

A TLS signature algorithm is related to authentication signatures.

Python 3.15 exposes separate APIs for these.

For example:

Python
context.set_groups(...)

controls groups.

While:

Python
context.set_client_sigalgs(...)
context.set_server_sigalgs(...)

control signature algorithms used for certificate-based authentication.

A post-quantum TLS deployment may involve both categories.

Do not treat them as interchangeable.

Hybrid TLS Is an Important Migration Strategy

Post-quantum migration does not necessarily mean replacing every classical algorithm immediately.

A practical transition can use hybrid mechanisms where classical and post-quantum cryptography are used together.

For example:

Code
Classical mechanism
        +
Post-quantum mechanism
        ↓
Hybrid protection

This allows organizations to test post-quantum interoperability while retaining established algorithms during the transition.

The exact hybrid groups and protocol combinations depend on the TLS implementation and the current standards supported by the endpoints.

Python 3.15's expanded group APIs make this area easier to inspect and configure, but the actual available post-quantum groups come from the underlying TLS implementation.

Check Both Ends of the TLS Connection

A common testing mistake is to update only the client.

For example:

Code
Python 3.15 client
       ↓
Old TLS server

If the server does not support the selected group or signature algorithm, negotiation can fail.

A proper test uses:

Code
Modern client
       ↓
Modern server

and then tests compatibility against:

Code
Modern client
       ↓
Existing server

This tells you whether the new cryptographic configuration works in both controlled and mixed environments.

Test TLS Negotiation Instead of Assuming It

After a TLS connection succeeds, Python 3.15 provides methods for inspecting the negotiated group and signature algorithm.

For example:

Python
with socket.create_connection(
    ("example.internal", 443)
) as sock:
    with context.wrap_socket(
        sock,
        server_hostname="example.internal"
    ) as tls:
        print("Cipher:", tls.cipher())
        print("Group:", tls.group())
        print("Server signature:", tls.server_sigalg())

The exact availability of these methods depends on the Python/OpenSSL combination. The Python 3.15 SSL documentation specifies OpenSSL requirements for them.

This is much better than assuming that because the handshake succeeded, the expected cryptographic mechanism was actually negotiated.

Why Certificate Compatibility Matters

TLS authentication normally involves certificates.

Post-quantum signatures create a much larger compatibility question:

Code
Client
 ↓
TLS handshake
 ↓
Server certificate
 ↓
Certificate signature
 ↓
Signature algorithm

Not every existing certificate-management system supports post-quantum public-key algorithms.

Even if OpenSSL can perform ML-DSA operations, your certificate authority, load balancer, reverse proxy, cloud service, or hardware security module may not support the same workflow.

That is why a post-quantum migration should be tested across the complete infrastructure rather than only inside Python.

ML-DSA Is Not ML-KEM

Another common mistake is mixing up signature and key-establishment algorithms.

ML-DSA is used for:

Code
Digital signatures
Authentication

ML-KEM is used for:

Code
Key encapsulation
Key establishment

They solve different problems.

A simplified TLS picture is:

Code
ML-KEM
    ↓
Establish shared secret

ML-DSA
    ↓
Authenticate signatures

Do not replace one with the other.

A post-quantum TLS design may need both types of cryptographic mechanisms.

Testing Application APIs

Suppose your API currently signs requests.

A traditional design might look like:

Code
Client
  ↓
Request body
  ↓
Signature
  ↓
Server
  ↓
Verify signature

To test ML-DSA, keep the protocol structure the same:

Code
Client
  ↓
Request body
  ↓
ML-DSA signature
  ↓
Server
  ↓
ML-DSA verification

Then measure:

Code
Request size
Signature size
Signing time
Verification time
Latency
Throughput

This tells you whether the algorithm is practical for the API rather than simply proving that the cryptographic operation works.

Be Careful With Headers

Large signatures can create practical problems when placed in HTTP headers.

For example:

Code
X-Signature: <large signature>

may work in a test environment.

But real systems have limits around:

A signature that is several kilobytes long should therefore be tested through the complete HTTP stack.

In many systems, placing the signature in the request body may be a better protocol design.

The correct choice depends on the API.

Test Storage Requirements

Post-quantum keys and signatures can be larger than their classical counterparts.

This can affect:

Code
Database columns
Cache entries
Message queues
Object storage
Audit logs
Token size
Network traffic

For example, if your database currently stores:

Code
signature VARBINARY(256)

that schema may be too small for ML-DSA signatures.

Do not change the schema based only on the algorithm name.

Measure the actual serialized key and signature sizes for the exact format your application uses.

Do Not Benchmark Only Cryptographic Operations

A microbenchmark may show:

Code
ML-DSA signing = X ms

but that is not enough.

The actual application cost may include:

Code
Serialize payload
      ↓
Generate signature
      ↓
Encode signature
      ↓
Build HTTP request
      ↓
Network transfer
      ↓
Server parsing
      ↓
Decode signature
      ↓
Verify

For APIs, benchmark the complete workflow.

This is where larger post-quantum signatures can become noticeable.

Security Testing Checklist

When evaluating ML-DSA, test more than performance.

Valid Signature

Code
Correct key
Correct message
Correct context

must verify successfully.

Modified Message

Code
Original signature
Modified message

must fail.

Modified Signature

Code
Modified signature
Original message

must fail.

Wrong Public Key

The signature must not verify with another key.

Wrong Context

A signature created with one context must not verify under another context.

Serialization Round Trip

Code
Generate
→ Serialize
→ Load
→ Sign
→ Verify

must work.

Cross-Implementation

Test:

Code
Python → OpenSSL
OpenSSL → Python

when your chosen formats and libraries support the required interoperability.

Common Mistakes

Assuming Python 3.15 Has a Built-In ML-DSA API

It does not provide a high-level MLDSA... signing API in the standard library.

For application-level signing, use a cryptographic library that supports ML-DSA.

Ignoring the OpenSSL Version

Python's SSL capabilities depend heavily on the underlying OpenSSL version.

Confusing ML-DSA With ML-KEM

They solve different cryptographic problems.

Testing Only Key Generation

A generated key does not prove that your complete application workflow works.

Ignoring Signature Size

Several-kilobyte signatures can affect APIs, storage, and network traffic.

Testing Only One TLS Endpoint

Both sides need compatible cryptographic capabilities.

Hard-Coding Algorithm Names

Available algorithms depend on the Python build, OpenSSL version, providers, and configuration.

Inspect capabilities at runtime where appropriate.

Designing Your Own Cryptographic Protocol

Do not invent a custom signature format or hybrid scheme simply to experiment with ML-DSA.

Use established standards and well-reviewed libraries.

A Practical Migration Plan

If you are considering post-quantum signatures for an existing Python application, start with a small test environment.

Phase 1: Inventory

List where your application currently uses public-key cryptography:

Code
TLS
API authentication
JWT-like tokens
Document signing
Software updates
Certificates
Identity systems

Phase 2: Dependency Check

Record:

Code
Python version
OpenSSL version
cryptography version
TLS proxy
Load balancer
Certificate authority
HSM/KMS

Phase 3: Application Test

Implement ML-DSA signing and verification using a supported library.

Measure:

Code
Signing
Verification
Key generation
Signature size
Serialization

Phase 4: TLS Test

Check:

Code
Available groups
Available signature algorithms
Negotiated group
Negotiated signature algorithm
Certificate compatibility

Phase 5: Interoperability

Test multiple clients and servers.

Do not stop after one successful connection.

Phase 6: Production-Like Load Test

Measure:

Code
Latency
Throughput
CPU
Memory
Network bandwidth
Request size
Failure rate

Phase 7: Gradual Rollout

If the results are good, introduce the new cryptographic configuration gradually.

Keep a fallback path until interoperability is proven.

Final Thoughts

Post-quantum cryptography is not something that can be adopted by changing one line of Python code.

ML-DSA is a good example of why.

At the application layer, you need a library that exposes the algorithm and a secure key-management strategy. At the TLS layer, you need compatible OpenSSL capabilities, supported TLS groups and signature algorithms, compatible certificates, and endpoints that can actually negotiate the configuration.

Python 3.15 improves the TLS side by exposing more control over groups and signature algorithms. That is useful because developers can inspect what the TLS stack is doing instead of treating the handshake as a black box.

For application-level signatures, current cryptography releases provide direct ML-DSA APIs for the three standardized parameter sets.

The safest approach is to treat ML-DSA as an engineering migration rather than simply a cryptographic experiment.

Start with:

Code
Can I generate a key?
Can I sign?
Can I verify?
Can I serialize the key?
Can another implementation verify it?
Can my TLS stack negotiate the required algorithms?
Can my infrastructure handle the larger keys and signatures?
Can my application handle the performance and network cost?

Once those questions have good answers, you have something much more valuable than a successful demo: a realistic understanding of whether post-quantum signatures fit your application.

Summary

ML-DSA is a post-quantum digital signature algorithm standardized in FIPS 204. Python 3.15 is relevant to post-quantum testing because its ssl module provides more control over TLS groups and signature algorithms, while the underlying OpenSSL version determines which cryptographic capabilities are actually available.

For application-level signing, a library such as cryptography can provide ML-DSA-44, ML-DSA-65, and ML-DSA-87.

Before using ML-DSA in production, test more than signing and verification. Check key serialization, signature sizes, API payloads, TLS negotiation, certificates, interoperability, CPU usage, network overhead, and infrastructure compatibility.

The main lesson is simple: post-quantum readiness is a system-level problem, not just a Python code change.