Home Tutorials Standard Library

Standard Library

Python's socket Module: A Beginner's Guide to TCP Sockets

Pyford Notes July 6, 2026 8 min read
Key points
  • socket.socket(socket.AF_INET, socket.SOCK_STREAM) creates a TCP/IPv4 socket; the two constants pick the address family and the connection style.
  • A server needs three separate calls — bind(), listen(), accept() — and accept() hands back a brand-new socket for talking to that one client.
  • Sockets send and receive raw bytes, not str; text has to be .encode()d going out and .decode()d coming back.
  • recv() can return fewer bytes than requested at any time — TCP is a byte stream with no built-in message boundaries, so reading in a loop until you have what you expect is not optional.

Creating a socket object

A socket is created with an address family and a socket type. For ordinary internet TCP connections, that's AF_INET (IPv4) and SOCK_STREAM (a reliable, ordered byte stream):

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

At this point the socket exists but isn't connected to anything. What happens next depends on whether this process is going to initiate a connection (a client) or wait for one (a server) — the API branches from here, but the underlying object is the same type either way. Sockets support the context manager protocol, so wrapping one in a with block guarantees it's closed even if an exception interrupts the exchange partway through.

A minimal TCP client

A client calls connect() with the server's address, then sends and receives bytes:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("example.com", 80))
    s.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
    response = s.recv(4096)
    print(response[:200])

sendall() is worth using over plain send() for outgoing data of any real size: send() is allowed to write only part of the buffer and return the count it managed, leaving the caller to resend the rest, while sendall() loops internally until everything is written or an error occurs. This is a real trap in code copied from older examples that assume send() always transmits the whole buffer in one call.

A minimal TCP server: bind, listen, accept

A server reserves a local address, starts listening for incoming connections, then blocks on accept() until a client connects:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(("0.0.0.0", 9000))
    server.listen()
    print("listening on 9000")
    conn, addr = server.accept()
    with conn:
        print("connected by", addr)
        data = conn.recv(1024)
        conn.sendall(data.upper())

Two details trip up first attempts here. First, accept() returns a completely separate socket object (conn) for talking to that specific client — the original server socket stays free to accept more connections and is never used for data transfer itself. Second, SO_REUSEADDR is worth setting before bind() during development, since without it, restarting a script that just crashed can fail with "address already in use" while the OS finishes releasing the port from the previous run.

Why recv() needs a loop

TCP delivers a stream of bytes with no concept of "messages" — the boundaries you see when calling send() once per logical message are not preserved on the receiving end. A single recv(4096) call can return 4096 bytes, 12 bytes, or the tail end of a message that started in a previous call, entirely depending on network timing:

def recv_exact(sock, n):
    chunks = []
    remaining = n
    while remaining > 0:
        chunk = sock.recv(remaining)
        if not chunk:
            raise ConnectionError("socket closed before all data arrived")
        chunks.append(chunk)
        remaining -= len(chunk)
    return b"".join(chunks)

Real protocols solve this by defining explicit message boundaries — a fixed-size length prefix packed with the struct module before the payload, or a delimiter the receiver scans for. Skipping this and assuming one recv() equals one logical message is the single most common bug in hand-rolled socket code.

Blocking calls, timeouts, and closing sockets

By default, accept(), recv(), and connect() all block the calling thread until something happens. settimeout(seconds) makes blocking calls raise socket.timeout instead of waiting forever, which matters for anything that shouldn't hang indefinitely on a dead peer. A server that needs to handle more than one client at once has to hand each accepted connection off to its own worker — commonly a thread per connection via the threading module, or an event loop built on select/asyncio for higher connection counts than one thread per client can comfortably support. Closing a socket (or exiting its with block) releases the OS-level file descriptor; leaving sockets unclosed in a long-running process is a slow but real resource leak. The official sockets HOWTO is the standard reference once framing and concurrency questions get more involved than a single request/response exchange.