The Python ssl module adds TLS to network sockets, providing encryption in transit and certificate-based peer authentication. It is used beneath HTTPS clients and can secure custom protocols built on TCP. Simply enabling encryption is not enough: a client must validate the certificate chain, verify the hostname, and use an appropriate protocol policy.
This guide introduces SSLContext, TLS clients and servers, public and private certificate authorities, minimum versions, SNI, timeouts, connection inspection, and mutual TLS. The examples use the standard library, although HTTP applications should normally prefer a higher-level client that integrates these decisions correctly.
TLS, SSL, and Python
The historical name SSL remains in the API, but old SSL protocols are broken and obsolete. Modern code should negotiate TLS with PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER. Python uses the installed OpenSSL library, so features and error messages can vary by platform.
Review the Python TCP client guide before adding TLS. TCP establishes the byte stream; the TLS handshake then negotiates protocol version, cipher, and certificates.
A secure TLS client
The recommended starting point for a general client is ssl.create_default_context(). It loads system certificate authorities, requires a valid chain, and verifies the hostname.
import socket
import ssl
hostname = "www.python.org"
context = ssl.create_default_context()
with socket.create_connection((hostname, 443), timeout=10) as tcp_socket:
with context.wrap_socket(
tcp_socket,
server_hostname=hostname,
) as tls_socket:
print(tls_socket.version())
print(tls_socket.cipher())
server_hostname is essential. It enables SNI so the server can select the correct certificate and also supplies the name that must match the certificate.
Why CERT_NONE is unsafe
Disabling verification keeps the traffic encrypted, but allows any certificate. A network attacker can present a certificate under their control, complete the handshake, and read or modify the connection.
# Do not use this in production.
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
Certificate failures should be fixed at the infrastructure layer: missing intermediate certificate, wrong hostname, expired certificate, incorrect system clock, or private CA not installed. Suppressing verification only hides the defect.
Building an explicit context
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.load_default_certs()
assert context.verify_mode == ssl.CERT_REQUIRED
assert context.check_hostname is True
PROTOCOL_TLS_CLIENT enables certificate and hostname verification by default. TLS 1.0 and 1.1 are obsolete. Set TLS 1.2 as the minimum when an explicit application policy is required; TLS 1.3 will be negotiated when both peers support it.
Private and corporate certificate authorities
Internal services may use a private CA. Load the trusted CA instead of disabling verification:
context = ssl.create_default_context(
cafile="company-root-ca.pem",
)
Distribute the CA over an authenticated channel and define a rotation process. Pinning only the leaf certificate without a renewal strategy can cause avoidable outages.
Sending application data
After the handshake, use sendall() and recv() like a normal socket. TLS is still a byte stream and does not preserve application-message boundaries.
request = (
"GET / HTTP/1.1\r\n"
f"Host: {hostname}\r\n"
"Connection: close\r\n\r\n"
).encode("ascii")
tls_socket.sendall(request)
chunks = []
while chunk := tls_socket.recv(16_384):
chunks.append(chunk)
response = b"".join(chunks)
For real HTTP work, use a library that handles redirects, proxies, cookies, response limits, and protocol details. See consuming REST APIs in Python.
A TLS server
A server uses PROTOCOL_TLS_SERVER and loads its certificate chain and private key.
import socket
import ssl
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.load_cert_chain(
certfile="certchain.pem",
keyfile="private.key",
)
with socket.socket() as listener:
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("127.0.0.1", 8443))
listener.listen()
with context.wrap_socket(listener, server_side=True) as secure_listener:
connection, address = secure_listener.accept()
with connection:
print(address, connection.version())
This is educational code. Production servers need concurrency, limits, structured logging, graceful shutdown, certificate renewal, and resource protection. The simple HTTP server guide explains why standard-library demos are not production web servers.
Protecting the private key
The private key should be readable only by the required process. Never commit it, bake it into a public container image, or print it. Many systems terminate TLS at a trusted proxy, load balancer, or managed service that automates key protection and renewal.
load_cert_chain() accepts a password callback for encrypted keys. Avoid interactive prompts in services; obtain the password from a protected secret source.
Mutual TLS
With mTLS, the server also requires and validates a client certificate.
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain("server.pem", "server.key")
server_context.load_verify_locations(cafile="client-ca.pem")
server_context.verify_mode = ssl.CERT_REQUIRED
A certificate proves possession of a key issued by an accepted CA. The application must still map that identity to permissions and maintain issuance, revocation, and rotation.
Client certificate configuration
client_context = ssl.create_default_context(cafile="server-ca.pem")
client_context.load_cert_chain("client.pem", "client.key")
Do not reuse one client private key everywhere when individual revocation is required.
Inspecting the peer and negotiated session
certificate = tls_socket.getpeercert()
print(certificate.get("subject"))
print(certificate.get("issuer"))
print(certificate.get("notAfter"))
print(tls_socket.version())
print(tls_socket.cipher())
Since Python 3.13, get_verified_chain() exposes the verified certificate chain, while get_unverified_chain() returns the raw chain sent by the peer. Never use an unverified chain as evidence of trust.
Timeouts and exceptions
Set connection and operation timeouts. Handle SSLCertVerificationError separately when diagnostics need the verification code and message.
try:
# connect and complete the handshake
pass
except ssl.SSLCertVerificationError as error:
print(error.verify_code, error.verify_message)
except (ssl.SSLError, OSError) as error:
print(f"TLS or network failure: {error}")
Do not log keys, bearer tokens, or confidential payloads. Hostname, negotiated version, OpenSSL reason, and a correlation ID are normally enough.
Non-blocking TLS sockets
With non-blocking sockets, a TLS read may require the transport to become writable and a write may require more incoming data. Handle SSLWantReadError and SSLWantWriteError through an event selector. For complex applications, asyncio or a networking framework manages many of these details.
Cipher suites and TLS 1.3
Modern Python contexts already select strong defaults. Avoid copying old cipher strings from tutorials. TLS 1.3 cipher configuration differs inside OpenSSL and is not controlled by every legacy option. Change cipher policy only for a reviewed compliance or interoperability requirement.
Self-signed certificates
A self-signed certificate can work for a local test or controlled private network if it is explicitly distributed as a trust anchor. Accepting one specific trusted certificate is different from accepting every certificate. Never solve a trust error by switching to CERT_NONE.
Common mistakes
The most dangerous mistakes are disabling verification, turning off hostname checks, omitting server_hostname, allowing obsolete TLS, using expired certificates, sharing private keys, skipping timeouts, and assuming encryption without authentication is secure.
Best practices
Start with create_default_context(). Require a valid chain and hostname. Use TLS 1.2 or later, keep Python and OpenSSL updated, configure timeouts, protect keys, automate renewal, and test expiration alerts. Maintain separate CAs and identities by environment for mTLS.
Conclusion
The ssl module can build TLS connections directly, but security depends on certificate verification, hostnames, protocol versions, and key management. Python’s modern defaults provide a strong base. Do not disable them to work around errors; repair the trust chain and prefer higher-level libraries whenever possible.
Read the official ssl documentation and RFC 8446 for TLS 1.3.







