Python Basics: Exchange Bytes and Detect EOF with socketpair
A pair of connected local sockets lets you test stream communication without opening a listening port. This example sends a short request, closes only the sending direction, and reads until the peer reports end of stream.
Prepare a scratch directory
Tested on Linux with CPython 3.12.14. You need Bash, Python, a text editor, and the standard mktemp, rm, and rmdir utilities. Run without -O so the assertions remain enabled. No external packages are required.
Keep the same terminal open. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as demonstrate-socketpair.py:
import socket
sender, receiver = socket.socketpair()
with sender, receiver:
sender.settimeout(2)
receiver.settimeout(2)
sender.sendall(b"red|blue")
sender.shutdown(socket.SHUT_WR)
chunks = []
while True:
chunk = receiver.recv(3)
if not chunk:
break
chunks.append(chunk)
message = b"".join(chunks)
assert message == b"red|blue"
print("Received:", message.decode("ascii"))
print("EOF:", receiver.recv(3) == b"")
receiver.sendall(b"ok")
receiver.shutdown(socket.SHUT_WR)
reply = bytearray()
while chunk := sender.recv(2):
reply.extend(chunk)
assert reply == b"ok"
print("Reply after half-close:", reply.decode("ascii"))Run it:
python3 -B demonstrate-socketpair.pyObserved output:
Received: red|blue
EOF: True
Reply after half-close: okRead the result
The receive size of three is an upper bound, not a message size. Joining all chunks makes the result independent of how the stream splits them. A nonempty receive request returning empty bytes signals EOF. It does not mean “no data yet.”
Calling shutdown(socket.SHUT_WR) leaves the other direction available: the sender still receives the reply. The context managers close both sockets when the block exits.
Apply it carefully
This protocol uses EOF as its boundary. A long-lived connection needs another framing rule, such as a length prefix. The two-second socket timeouts bound individual blocking operations; they are not a whole-session deadline. Tiny payloads fit the local buffers here. For large bidirectional transfers, sending everything before reading can deadlock; coordinate readers and writers and bound accumulated data.
The official socket reference describes the relevant APIs and their limits.
Clean up
Remove the saved source, return to the starting directory, and remove the empty practice directory:
rm -- demonstrate-socketpair.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect its remaining contents before removing anything else.
Acknowledgements
This post was created with assistance from AI.