The socket module provides access to the BSD socket interface, the low-level networking primitive used to send and receive data across a network. A socket is an endpoint of a communication channel, identified by an address and port, supporting protocols such as TCP (reliable streams) and UDP (datagrams).
The phone line between programs
What you will learn: what a socket is, the TCP-vs-UDP distinction, and how to write a basic TCP client that connects and exchanges bytes.
How to read this tab: Sockets send bytes, not strings — encode before sending, decode after receiving. That trips up everyone first.
A socket is a phone line between two programs over a network. One side listens for a call (the server), the other dials in (the client), and once connected they send bytes back and forth. Every web request, chat message, and API call ultimately rides on a socket.
Sockets send bytes, not strings. Everything over a socket is bytes — encode before sending ("hi".encode()) and decode after receiving (data.decode()). Forgetting this is the most common first-time socket error.
What a socket is
A socket is an endpoint for network communication, identified by an IP address and a port number. The two main types are TCP (a reliable, ordered byte stream — like a phone call) and UDP (fast, connectionless messages — like postcards that may arrive out of order or not at all).
import socket
# Create a TCP socket
# AF_INET = IPv4 addressing, SOCK_STREAM = TCP (reliable stream)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# A UDP socket would use SOCK_DGRAM instead
# u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Look up the IP address behind a hostname (DNS)
print(socket.gethostbyname("example.com")) # e.g. '93.184.216.34'
# Your own machine's hostname
print(socket.gethostname())
# Always close a socket when done (or use a with-block)
s.close()
# Ports below 1024 are "well-known" (80=HTTP, 443=HTTPS, 22=SSH).
# Use ports above 1024 for your own applications.Everything sent over a socket is bytes, not str. Encode before sending ("hello".encode()) and decode after receiving (data.decode()). Forgetting this is the most common first-time socket error.
recv(n) reads up to n bytes, not exactly n. Data arrives in chunks, so a single recv may return less than requested. Loop until you have the full message or recv returns empty bytes (which means the connection closed). Use sendall (not send) to transmit everything.
A TCP client
import socket
# Connect to a server and exchange data — using a with-block for cleanup
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("example.com", 80)) # (host, port) tuple
# Send an HTTP request (must be bytes)
request = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
s.sendall(request.encode()) # sendall sends ALL the bytes
# Receive the response in chunks until the server closes
chunks = []
while True:
data = s.recv(4096) # read up to 4096 bytes
if not data: # empty bytes = connection closed
break
chunks.append(data)
response = b"".join(chunks).decode("utf-8", errors="replace")
print(response[:200]) # first 200 chars of the response
# In real code you would use the 'requests' library or urllib — this
# shows what those libraries do underneath.✅ Beginner tab complete
- I know a socket is a network endpoint (address + port)
- I know TCP is a reliable stream; UDP is fast connectionless messages
- I can write a TCP client: connect, sendall, recv in a loop
- I always encode strings to bytes before sending
Servers, UDP, and timeouts
What you will learn: the bind/listen/accept/recv server lifecycle, UDP send/recv, and using timeouts to avoid blocking forever.
How to read this tab: The four-step server pattern (bind, listen, accept, recv/send) is the heart of every network server.
The four steps: bind, listen, accept, recv/send. This lifecycle is the heart of every network server. accept() blocks until a client connects and returns a new socket for that client. The basic loop serves one client at a time — real servers add threads, selectors, or asyncio.
A TCP server
A server binds to an address, listens for connections, accepts them one at a time, and communicates with each client. This four-step pattern — bind, listen, accept, recv/send — is the heart of every network server.
import socket
HOST = "127.0.0.1" # localhost — only this machine can connect
PORT = 65432 # a port above 1024
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
# Allow reusing the address immediately after the program restarts
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT)) # claim the address and port
server.listen() # start listening for connections
print(f"Listening on {HOST}:{PORT}")
while True:
# accept() blocks until a client connects, returning a NEW
# socket for that specific client plus their address
conn, addr = server.accept()
with conn:
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break # client disconnected
# Echo the data back, uppercased
conn.sendall(data.upper())The loop above serves connections sequentially — a second client waits until the first disconnects. Real servers handle many clients concurrently using threads, selectors, or asyncio. For anything beyond learning, a framework built on these primitives is the right choice.
TCP for order and reliability; UDP for speed. TCP guarantees delivery and ordering (web, email, files). UDP is faster but does neither (video, games, DNS). UDP needs no connect/accept — you sendto an address directly. Always set a timeout to avoid blocking forever.
UDP and connection details
import socket
# UDP — connectionless. No connect/accept; you send to an address directly.
# UDP server
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(("127.0.0.1", 9999))
data, addr = server.recvfrom(1024) # returns (data, sender_address)
server.sendto(b"reply", addr) # send back to that address
# UDP client
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b"hello", ("127.0.0.1", 9999)) # no connection needed
reply, _ = client.recvfrom(1024)
# Timeouts prevent a socket from blocking forever
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5.0) # raise socket.timeout after 5 seconds
try:
s.connect(("example.com", 80))
except socket.timeout:
print("Connection timed out")
except socket.gaierror:
print("Could not resolve the hostname (DNS failure)")
finally:
s.close()
# TCP guarantees order and delivery; UDP is faster but does neither.
# Use TCP for web, email, files. Use UDP for video, games, DNS.send() may transmit only part of the data and returns how many bytes were sent; you would have to loop. sendall() keeps going until everything is sent (or it errors). Use sendall unless you have a specific reason not to.✅ Intermediate tab complete
- I can write a TCP server: bind, listen, accept, recv/send
- I know this basic server handles one client at a time
- I can use UDP with sendto/recvfrom
- I use settimeout to prevent indefinite blocking
Non-blocking I/O, selectors, and asyncio
What you will learn: blocking vs non-blocking sockets, using selectors to handle many connections in one thread, and the ladder up to socketserver and asyncio.
How to read this tab: Read to understand what every HTTP library and web framework does underneath.
Blocking vs non-blocking, selectors, and the path to asyncio
By default sockets are blocking: recv, accept, and connect halt the thread until they complete. This is simple but does not scale — one thread per connection is expensive at thousands of connections. The solutions are non-blocking sockets combined with an I/O multiplexer (selectors), or the higher-level asyncio framework built precisely on these primitives.
import socket, selectors
# A selector lets ONE thread watch many sockets, reacting only to
# those that are ready — the foundation of high-concurrency servers.
sel = selectors.DefaultSelector()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 65432))
server.listen()
server.setblocking(False) # non-blocking mode
sel.register(server, selectors.EVENT_READ, data=None)
def event_loop():
while True:
events = sel.select(timeout=None) # wait for ANY socket to be ready
for key, mask in events:
if key.data is None:
# The listening socket is ready -> accept a new client
conn, addr = key.fileobj.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, data=addr)
else:
# A client socket is ready -> read without blocking
data = key.fileobj.recv(1024)
if data:
key.fileobj.sendall(data.upper())
else:
sel.unregister(key.fileobj)
key.fileobj.close()
# socketserver — the stdlib's higher-level server framework
import socketserver
class EchoHandler(socketserver.BaseRequestHandler):
def handle(self):
data = self.request.recv(1024)
self.request.sendall(data.upper())
# with socketserver.ThreadingTCPServer(("127.0.0.1", 0), EchoHandler) as srv:
# srv.serve_forever() # threaded server, one thread per client
# socket.create_connection — convenience for TCP clients (handles DNS,
# IPv4/IPv6 fallback, and timeout in one call)
with socket.create_connection(("example.com", 80), timeout=5) as s:
s.sendall(b"GET / HTTP/1.0\r\n\r\n")The progression in Python networking is a ladder of abstraction: raw socket for full control, selectors for efficient multiplexing of many connections in one thread, socketserver for a threaded or forking server framework, and asyncio for high-concurrency asynchronous I/O with async/await syntax. Above all of these sit application libraries — http.client, urllib, requests, and full web frameworks — that almost everyone uses in practice. Understanding the socket layer matters not because you will write raw sockets often, but because it demystifies everything built on top: when a request hangs, a connection resets, or a timeout fires, the behaviour makes sense once you know the bind/listen/accept/recv lifecycle underneath.
✅ Expert tab complete
- I know blocking sockets halt the thread until they complete
- I can use selectors to watch many sockets in one thread
- I know socketserver provides a threaded server framework
- I know asyncio is built on these same socket primitives