Python Module 16 – Practical Python: text, CLIs, web, automation and security
Socket programming basics: TCP and UDP
Open TCP clients and servers with Python's socket module, read whole messages despite partial reads, set timeouts, and see when UDP or HTTP fits better.
What you will learn
- Create a TCP server and client with create_server() and create_connection()
- Read a whole message despite partial reads, with a loop and a framing rule
- Limit every blocking call with a timeout, and close sockets with a with statement
- Send and receive UDP datagrams, and handle a datagram that never arrives
- Choose between raw sockets, asyncio streams and HTTP for a program
Before you start
What you’ll learn · Before you start
What you will learn
- Create a TCP server and client with create_server() and create_connection()
- Read a whole message despite partial reads, with a loop and a framing rule
- Limit every blocking call with a timeout, and close sockets with a with statement
- Send and receive UDP datagrams, and handle a datagram that never arrives
- Choose between raw sockets, asyncio streams and HTTP for a program
Before you start
On this page
One 7-byte word sent over a TCP One end of a network connection as a program sees it: an object with methods such as send() and recv(). Python's socket module makes TCP sockets, which carry a stream of bytes, and UDP sockets, which carry datagrams. can reach recv(1024) as 7 bytes at once or as seven calls of 1 byte
each: TCP delivers a stream of bytes, not messages. That fact, a time limit on every wait and a clean
close are most of what makes socket code work outside a test.
Python’s socket module gives you both ends. socket.create_server() opens a server socket that listens on an
address and a port, socket.create_connection() connects a client, and a with statement closes each of them. You
will run a TCP echo server, read whole messages from a stream that splits them, and limit every wait. The browser
runner cannot open sockets, so the network examples were recorded on 127.0.0.1, this computer’s own address.
A TCP server listens, a client connects, and both send bytes
A TCP server uses two kinds of socket: one that listens on a port, and a new one for every connection it accepts.
Step through the first client of echo.py:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The server listens. create_server(("127.0.0.1", 0)) binds a socket to a free port of this machine and listens. It accepts connections; it never carries data itself.
- The client connects. create_connection(("127.0.0.1", port), timeout=5) connects to that port. The operating system completes TCP's handshake before accept() even returns.
- accept() makes a new socket. accept() returns a new socket for this one connection, with the client's address. The listening socket goes back to waiting for the next client.
- Bytes in. The client calls sendall(b"namaste"). The connection socket's recv(1024) returns the 7 bytes; TCP delivers them in order, with no message boundaries of its own.
- Bytes out. The server sends the bytes back in capitals with sendall(). The client reads exactly 7 bytes, looping until they have all arrived.
- The client closes. Leaving the client's with block closes its socket. The server's next recv() returns b"", the sign that the other side has gone, and its loop ends.
"""A TCP echo server and two clients on 127.0.0.1: listen, accept, send, receive and close."""
import socket
import threading
events = [] # the server's side, printed once the clients are done
def recv_exactly(sock, n):
"""Read exactly n bytes: recv() returns what has arrived, which may be less."""
data = b""
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
raise ConnectionError("the peer closed before the whole reply arrived")
data += chunk
return data
def serve(listener):
for _ in range(2): # two clients, one after the other
conn, (host, _port) = listener.accept() # a new socket for this connection only
with conn:
events.append(f"accepted a connection from {host}")
while data := conn.recv(1024): # b"" means the client has closed its side
events.append(f"received {data!r}")
conn.sendall(data.upper())
events.append("recv() returned b'': the client has gone")
with socket.create_server(("127.0.0.1", 0)) as listener: # port 0: the system picks a free port
port = listener.getsockname()[1]
server = threading.Thread(target=serve, args=(listener,))
server.start()
for word in [b"namaste", b"vanakkam"]:
with socket.create_connection(("127.0.0.1", port), timeout=5) as client:
client.sendall(word)
print(f"client sent {word!r}, got {recv_exactly(client, len(word))!r}")
server.join()
print("server:", *events, sep="\n ") Output
client sent b'namaste', got b'NAMASTE' client sent b'vanakkam', got b'VANAKKAM' server: accepted a connection from 127.0.0.1 received b'namaste' recv() returned b'': the client has gone accepted a connection from 127.0.0.1 received b'vanakkam' recv() returned b'': the client has gone
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 echo.py
The server bound to 127.0.0.1, the
The address 127.0.0.1 (::1 in IPv6) that always means this computer. A server bound to it accepts connections only from programs on the same machine.,
so only programs on this computer could connect, and port 0 let the system pick a free
A number from 1 to 65535 that, with the address, names one listening program on a computer, such as 443 for HTTPS..
accept() returned a new socket for each client, while the listening socket went on waiting. sendall() sends every
byte or raises, unlike send(), which may send only part and returns how much. When recv() returns b"", the
other side has closed its end, and the documentation states it plainly: a returned empty bytes object indicates that
the client has disconnected.
Key idea
A server listens with one socket and talks to each client on a new one; recv() returning b"" means the other
side has closed.
Checkpoint
Check: TCP servers, clients and accept()
recv() returns what has arrived, not a message
A message sent with one sendall() can arrive in several pieces, and two messages can arrive in one. RFC 9293 says
that TCP segments often do not correspond one for one to the send calls of the application, so the receiver gets a
stream of bytes with no mark where one send ended. Step through a read of the
5-byte message hello behind a 2-byte length, which arrives in three chunks. The list chunks stands for what
three recv() calls would return:
Program · trace/partial_reads.py
chunks = [b"\x00", b"\x05he", b"llo"] # what three recv() calls return: TCP split the message
received = b""
size = None # the length prefix is not known yet
while size is None or len(received) < 2 + size:
received += chunks.pop(0) # recv() returns whatever has arrived
if size is None and len(received) >= 2:
size = int.from_bytes(received[:2]) # the prefix is complete: the message length is known
print("length prefix:", size)
print("message:", received[2:2 + size]) Variables
- changed in this step
Printed so far
Nothing printed yetlength prefix: 5 message: b'hello'
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 17 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
chunks = [b"\x00", b"\x05he", b"llo"]makeschunksa list of 3 items. - Line 2.
received = b""setsreceivedtob''. - Line 3.
size = NonesetssizetoNone. - Line 4.
while size is None or len(received) < 2 + size:tests its condition: it is true (sizeisNoneandreceivedisb''), so the body runs. - Line 5.
received += chunks.pop(0)changesreceivedfromb''tob'\x00'. Key point The first recv() finds onlyb"\x00": one byte, although the length prefix alone is two. That is normal for TCP, not an error. - Line 6.
size is None and len(received) >= 2is false (sizeisNoneandreceivedisb'\x00'), so Python skips the block underif size is None and len(received) >= 2:. - Line 4.
while size is None or len(received) < 2 + size:tests its condition: it is true (sizeisNoneandreceivedisb'\x00'), so the body runs again (pass 2). - Line 5.
received += chunks.pop(0)changesreceivedfromb'\x00'tob'\x00\x05he'. - Line 6.
size is None and len(received) >= 2is true (sizeisNoneandreceivedisb'\x00\x05he'), so the block underif size is None and len(received) >= 2:runs. - Line 7.
size = int.from_bytes(received[:2])setssizeto5(it wasNone). Nowreceivedholdsb"\x00\x05he". The bytes\x00\x05are the number 5 in big-endian order, so the message that follows is 5 bytes long, and only 2 of them are here. - Line 4.
while size is None or len(received) < 2 + size:tests its condition: it is true (sizeis5andreceivedisb'\x00\x05he'), so the body runs again (pass 3). - Line 5.
received += chunks.pop(0)changesreceivedfromb'\x00\x05he'tob'\x00\x05hello'. The third recv() bringsb"llo". With the 2-byte prefix and 5 bytes of message,receivedholds 7 bytes, so the loop test is false at last. - Line 6.
size is None and len(received) >= 2is false (sizeis5andreceivedisb'\x00\x05hello'), so Python skips the block underif size is None and len(received) >= 2:. - Line 4.
while size is None or len(received) < 2 + size:tests its condition: it is false (sizeis5andreceivedisb'\x00\x05hello'), so the loop ends. - Line 8.
print("length prefix:", size)printslength prefix: 5. - Line 9.
print("message:", received[2:2 + size])printsmessage: b'hello'. That was the last line, so the program ends. Three recv() calls, and none returned a whole message. A program that took one recv() as one message would have parsed half of it.
The loop could not know the length of the message until the second chunk had brought the whole prefix, and it went
on reading until it held all 7 bytes. recv_exactly() in echo.py is the same loop as a function: it asks each
time only for the bytes still missing, and treats b"" as the connection closing in the middle. Every protocol
over TCP also needs
framing: The rule a protocol uses to mark where each message ends in a stream of bytes, such as a newline after each message or a length before it. TCP keeps no record of where one write ended, so every protocol over it needs one. More in “Networking with asyncio streams”, a rule for where a message ends: a newline after each message, or a
length prefix: A fixed-size number written before each message that gives its size in bytes, often 4 bytes in big-endian order. The receiver reads the number, then exactly that many bytes, whatever they contain. More in “Networking with asyncio streams” before it, as here.
One recv() taken as one message
data = sock.recv(1024) followed by json.loads(data) works on your laptop, where short messages usually arrive
whole, and fails under load or over a slow network with half a message. Read with a loop until your framing says
the message is complete.
Checkpoint
Check: Partial reads and framing
Every wait gets a time limit, and every socket gets closed
By default a socket waits for ever: a peer that accepts a connection and then sends nothing holds recv() until
the program is stopped. settimeout(seconds) limits every blocking call on the socket, and the timeout argument
of create_connection() sets the same limit from the start. Since Python 3.10, socket.timeout is an alias of
TimeoutError. This run uses a connected pair of sockets inside one program, which needs no network:
"""Timeouts and closing, on a connected pair of sockets: a silent peer, a closed peer, and the with statement."""
import socket
import time
left, right = socket.socketpair() # two connected sockets in this program, so no network is needed
with left, right:
left.settimeout(0.2) # every blocking call on left now waits at most 0.2 s
start = time.monotonic()
try:
left.recv(1024) # right never sends anything
except TimeoutError as e:
print(f"silent peer: {type(e).__name__} ({e}) after {time.monotonic() - start:.1f} s")
right.close() # the peer closes its end
print("closed peer: recv() returned", left.recv(1024))
print("after the with block, left is closed:", left.fileno() == -1) Output
silent peer: TimeoutError (timed out) after 0.2 s closed peer: recv() returned b'' after the with block, left is closed: True
This output changes from run to run: the wait is measured on each run and may take a little longer than 0.2 s on a busy computer
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timeouts.py
The with block closed both sockets on the way out, as it would after an exception, and a closed socket reports
fileno() as −1. Five more tools come up as soon as programs talk for real:
- Framing choices: newline-ended messages are simple but break on a message that contains a newline; a length prefix carries any bytes but needs a size limit.
- Half-close:
shutdown(SHUT_WR)says that you have sent everything, while the socket can still read the answer. - Binding: 127.0.0.1 keeps a server to this machine,
''opens it on every interface, and one address and port take one server. - UDP: a
SOCK_DGRAMsocket sends single One self-contained message sent by UDP. It arrives whole or not at all, and datagrams may be lost, repeated or delivered in another order. that keep their boundaries but may be lost. UDP needs no connection and noaccept(), so one socket can hear many senders, and the program must cope with loss itself. - Higher levels: HTTP clients and asyncio streams do this work for you, and most programs should use them. HTTP fits any service that already speaks it, such as a web API, and asyncio streams fit a TCP protocol of your own that keeps many connections open.
Key idea
Give every socket a timeout and a with block, so that a silent peer cannot hold the program and every path closes
the socket.
Checkpoint
Check: Timeouts and closing
Practise: read whole messages from a stream that splits them
Write recv_exactly() and recv_line(). The sample tests use a fake socket that returns from 1 to 5 bytes at a
time, chosen by a seeded random generator, so your loops meet the same splits on every run. Once they pass, a
practice session starts from your weakest concept, and each answer is explained as soon as you give it.
Exercise · Medium · Python
Read whole messages from a stream that splits them
Write two functions in netio.py that read from anything with a recv(n) method, as a socket has. The tests pass in a fake socket that returns a random number of bytes, from 1 to 5, on each call, as a real network may, and b"" once its data has run out, as a closed connection does.
**recv_exactly(sock, n)** returns exactly n bytes. It calls recv() again until it has them all, asking each time only for the bytes still missing, and raises ConnectionError when recv() returns b"" before that. recv_exactly(sock, 0) returns b"".
**recv_line(sock, limit=1024)** returns the bytes up to and including the first b"\n". It must not read past the newline, so that the next call finds the next line. It raises ValueError when limit bytes have arrived without a newline, and ConnectionError when the connection closes first.
Starter code · netio.py
def recv_exactly(sock, n):
"""Exactly n bytes from sock; ConnectionError if it closes first."""
return sock.recv(n)
def recv_line(sock, limit=1024):
"""The bytes up to and including the first newline; ValueError past limit, ConnectionError if it closes."""
return sock.recv(limit) The sample tests · test_netio.py
import random
from netio import recv_exactly, recv_line
class ChunkySocket:
"""Returns from 1 to 5 bytes per recv(), chosen by a seeded random generator, then b"" when the data runs out."""
def __init__(self, data, seed=7):
self.data, self.rng, self.calls = data, random.Random(seed), 0
def recv(self, n):
self.calls += 1
size = min(n, self.rng.randint(1, 5))
part, self.data = self.data[:size], self.data[size:]
return part
class OneThenAsked:
"""The first recv() returns one byte, every later one as many as it is asked for."""
def __init__(self, data):
self.data, self.first = data, True
def recv(self, n):
size = 1 if self.first else n
self.first = False
part, self.data = self.data[:size], self.data[size:]
return part
def raises(error, call, *args):
try:
call(*args)
except error:
return True
return False
def test_exactly_collects_small_chunks():
"""recv_exactly() keeps calling recv() until it has n bytes"""
sock = ChunkySocket(b"0123456789abcdef")
assert recv_exactly(sock, 10) == b"0123456789"
assert recv_exactly(sock, 6) == b"abcdef"
assert sock.calls > 2
def test_exactly_asks_only_for_the_missing_bytes():
"""recv_exactly() never takes a byte of the next message"""
sock = OneThenAsked(b"abcdefgh")
assert recv_exactly(sock, 4) == b"abcd"
assert sock.data == b"efgh"
def test_exactly_zero_and_early_close():
"""zero bytes need no read, and a close before n bytes is an error"""
assert recv_exactly(ChunkySocket(b"abc"), 0) == b""
assert raises(ConnectionError, recv_exactly, ChunkySocket(b"abc"), 5)
def test_line_stops_at_the_newline():
"""recv_line() returns one line, and the next call returns the next"""
sock = ChunkySocket(b"ROLL 21\nROLL 22\n")
assert recv_line(sock) == b"ROLL 21\n"
assert recv_line(sock) == b"ROLL 22\n"
def test_line_limit_and_close():
"""too long without a newline, or closed first, is an error"""
assert raises(ValueError, recv_line, ChunkySocket(b"x" * 50), 20)
assert raises(ConnectionError, recv_line, ChunkySocket(b"no newline")) A hint
In recv_exactly(), keep a bytes buffer and loop while len(buf) < n, calling sock.recv(n - len(buf)). recv_line() can then read one byte at a time with recv_exactly(sock, 1), which can never take a byte of the next line.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Practice that adapts to you
Practise this lesson
9 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Framing messages, closing cleanly and binding with care Pro: opens with a Pro, Premium or Ultimate pass
A protocol over TCP needs more than a reading loop: a rule for where each message ends, a way to say that you are done, and a decision about who may connect at all. This part puts two framings on one stream and shows where each one breaks, half-closes a connection, and binds a server with care.
In this part:
- Two framings on one stream, and the limit each one needs
- Half-close: a shutdown ends the request, not the connection
- Binding: free ports, ports in use and who can connect
3 runnable examples (Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
UDP datagrams, and choosing the right level for a program Pro: opens with a Pro, Premium or Ultimate pass
UDP drops the stream: each send is one datagram, delivered whole or not at all, so the program has to handle what TCP would have handled for it. This part runs a UDP client that survives a lost datagram, works out what a retry is worth, and ends with a decision table for choosing between sockets, asyncio streams and HTTP.
In this part:
- A UDP ping that survives a lost datagram
- Raw sockets, asyncio streams or HTTP: a decision with reasons
1 runnable example (Python) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is the difference between a TCP socket and a UDP socket in Python?
A TCP socket (SOCK_STREAM) carries a connection: a server listens and accepts, each client gets a socket of its own, and the bytes arrive in order or the program gets an error. It is a stream with no message boundaries, so one send may arrive in several recv() calls and two sends in one, and the program reads until its own framing says that a message is complete. A UDP socket (SOCK_DGRAM) has no connection: each sendto() is one datagram, which keeps its boundaries but may be lost, repeated or reordered, and nobody tells the sender. Choose TCP when every message counts, and UDP when a lost message does no harm or the protocol is already UDP.
-
Fresher to mid
A client sends JSON messages over TCP and reads each reply with one recv(4096) followed by json.loads(). It passes every test on a laptop but now and then fails in production with half a message or extra data. Explain why, and rewrite the reading.
-
Mid to senior
A TCP server accepts one client at a time and reads from it until it closes. Once a day it stops answering everyone, although the process is alive and the CPU is idle. Find the cause, fix the server, and describe the test that would have shown it.
-
Mid to senior
An app sends one UDP datagram per event to a metrics collector. At peak hours the dashboards show 15 % fewer orders than the database does, and no errors appear anywhere. Explain what is happening and what you change.
Key takeaways
- A TCP server listens with
create_server(),accept()returns a new socket per client, andrecv()returningb""means the other side has closed. recv()returns what has arrived, so read in a loop until your framing, a newline or a length prefix, says the message is complete.- Give every socket a timeout and a
withblock, so that a silent peer cannot hold a program and a socket is closed on every path.
References
- Socket programming HOWTO (Python Software Foundation)
- socket: low-level networking interface (Python Software Foundation)
- struct: interpret bytes as packed binary data (Python Software Foundation)
- asyncio streams (Python Software Foundation)
- RFC 9293: Transmission Control Protocol (TCP) (IETF)
- RFC 768: User Datagram Protocol (IETF)
- RFC 8085: UDP usage guidelines (IETF)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress