TLS with the ssl Module
The standard library's ssl module wraps a socket with TLS, built on the platform's OpenSSL. It is the same engine behind requests and urllib, and it is what you reach for when you need a raw TLS connection - a custom protocol, a test server in a script, or SMTP/IMAP over TLS without a third-party client.
A Verifying Client
import ssl
import socket
ctx = ssl.create_default_context() # system CAs + hostname verification
with socket.create_connection(("example.com", 443)) as sock:
with ctx.wrap_socket(sock, server_hostname="example.com") as ssock:
print(ssock.version()) # TLSv1.3 (or 1.2)
print(ssock.cipher_name()) # e.g. TLS_AES_256_GCM_SHA384
ssock.sendall(b"GET / HTTP/1.0\r\n\r\n")
print(ssock.recv(200))
create_default_context() is the safe default: it loads the system CA bundle, requires a valid certificate, and checks the hostname. A SSLCertVerificationError means the chain, validity window, or name failed - treat it as a failure, not something to silence in production.
A TLS Server
import ssl
import socket
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("server.crt", "server.key") # cert + private key
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
with socket.create_server(("127.0.0.1", 8443)) as listener:
conn, addr = listener.accept()
with ctx.wrap_socket(conn, server_side=True) as tls:
print(tls.recv(1024))
For clients you serve, add ctx.load_verify_locations("ca.crt") and ctx.verify_mode = ssl.CERT_REQUIRED to require and validate client certificates (mutual TLS).
Self-Signed Certificates
Local development usually runs against self-signed certs. The honest options, in order of preference:
- Add the dev CA to the trust store - the system store, or your
ctxviaload_verify_locations- and keep verification on. - Use
ssl._create_unverified_context(), the stdlib's explicit escape hatch, for throwaway tests only. - Manually:
ctx.check_hostname = Falseplusctx.verify_mode = ssl.CERT_NONE- a smell in production code.
Reading the Negotiated Session
After wrapping, the socket reports what was actually negotiated: version(), cipher_name(), and getpeercert() (the peer certificate as a dict). Logging these in your test suite makes "we negotiated TLS 1.0 with a weak cipher" visible before it reaches production, and getpeercert() is how you extract the client identity in a mutual-TLS setup.
Summary
Build clients with create_default_context() plus wrap_socket(server_hostname=...), serve with PROTOCOL_TLS_SERVER and a loaded cert chain, and keep hostname verification on in production. Self-signed convenience belongs in the test suite, never in the request path.