Network Programming with sockets

Reviewed & published by Brayan K

A socket is an endpoint for sending and receiving bytes between programs over a network, and Python's socket module lets you build both the client and the server sides of that conversation.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Almost every networked tool you use — web browsers, chat apps, databases — speaks over sockets underneath. We'll build a tiny TCP echo server and client on localhost, and use socketpair() for a clean, deterministic demo.

What You'll Learn in This Lesson

1 socketpair(): The Simplest Connection

The cleanest way to see sockets in action is socket.socketpair(), which hands you two already-connected sockets — no IP, port, or server setup. Send bytes on one end, receive them on the other. Note .encode() and .decode() converting between strings and bytes:

import socket

# Two sockets already wired together
a, b = socket.socketpair()

a.sendall(b"ping")
print("b received:", b.recv(1024).decode())   # ping

b.sendall(b"pong")
print("a received:", a.recv(1024).decode())    # pong

a.close()
b.close()
print("Closed both ends")
import socket

a, b = socket.socketpair()

a.sendall(b"ping")
print("b received:", b.recv(1024).decode())

b.sendall(b"pong")
print("a received:", a.recv(1024).decode())

a.close()
b.close()
print("Closed both ends")

# ✅ Expected output:
# b received: ping
# a received: pong
# Closed both ends

2 A Tiny TCP Echo Server + Client

Now the real thing. A server bind()s to an address, listen()s, and accept()s a connection; a client connect()s and exchanges data. We run the server in a thread, let it pick a free port (port 0), and use an Event so the client only connects once the server is ready — keeping it fast and deterministic:

import socket
import threading

def echo_server(port_box):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(("127.0.0.1", 0))      # 0 = let the OS pick a free port
    server.listen(1)
    port_box.append(server.getsockname()[1])
    started.set()                       # signal: server is ready
    conn, addr = server.accept()        # wait for the client
    data = conn.recv(1024)
    conn.sendall(b"echo: " + data)      # send the reply back
    conn.close()
    server.close()

started = threading.Event()
port_box = []
t = threading.Thread(target=echo_server, args=(port_box,))
t.start()
started.wait()                          # don't connect until ready
port = port_box[0]

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", port))
client.sendall(b"hello")
reply = client.recv(1024).decode()
print("Server replied:", reply)         # echo: hello
client.close()
t.join()
print("Done")
import socket
import threading

def echo_server(port_box):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(("127.0.0.1", 0))
    server.listen(1)
    port_box.append(server.getsockname()[1])
    started.set()
    conn, addr = server.accept()
    data = conn.recv(1024)
    conn.sendall(b"echo: " + data)
    conn.close()
    server.close()

started = threading.Event()
port_box = []
t = threading.Thread(target=echo_server, args=(port_box,))
t.start()
started.wait()
port = port_box[0]

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", port))
client.sendall(b"hello")
reply = client.recv(1024).decode()
print("Server replied:", reply)
client.close()
t.join()
print("Done")

# Expected output:
# Server replied: echo: hello
# Done

3 Encode / Decode: Strings Across the Wire

The boundary between your Python strings and the network is always bytes. The pattern is simple and you'll repeat it constantly: .encode() before sendall, .decode() after recv:

import socket

left, right = socket.socketpair()

message = "concurrency rocks"
left.sendall(message.encode())          # str -> bytes
received = right.recv(1024).decode()    # bytes -> str

print("Sent:    ", message)
print("Received:", received)
print("Match:", message == received)

left.close()
right.close()
import socket

left, right = socket.socketpair()

message = "concurrency rocks"
left.sendall(message.encode())
received = right.recv(1024).decode()

print("Sent:    ", message)
print("Received:", received)
print("Match:", message == received)

left.close()
right.close()

# ✅ Expected output:
# Sent:     concurrency rocks
# Received: concurrency rocks
# Match: True

🎯 Your Turn: Fill in the Blanks

Replace each ___ so a string is encoded, sent across a socket pair, received, and decoded back.

import socket

left, right = socket.socketpair()

message = "sockets are bytes"
left.sendall(message.___())          # hint: str -> bytes
received = right.recv(1024).___()    # hint: bytes -> str

print("Match:", message == received)

left.close()
right.close()

# Expected output:
# Match: True

⚠️ Common Errors

❌ Sending a str instead of bytes

sock.sendall("hello")        # TypeError: a bytes-like object is required
sock.sendall("hello".encode())

❌ Printing raw recv() bytes

print(sock.recv(1024))       # b'hello'  (note the b prefix)

✅ Decode back to a string

print(sock.recv(1024).decode())   # hello

❌ Connecting before the server is listening

If the client runs before the server's listen(), the connection is refused. Use a synchronization signal like threading.Event (as in our demo) so the client waits until the server is ready.

🎯 Mini Challenge: Uppercase Server

Write a tiny server that receives a message and replies with it UPPERCASED. Run it in a thread, connect a client, send one message, and print the loud reply.

import socket
import threading

def upper_server(port_box, ready):
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(("127.0.0.1", 0))
    srv.listen(1)
    port_box.append(srv.getsockname()[1])
    ready.set()
    conn, _ = srv.accept()
    data = conn.recv(1024)
    conn.sendall(data.upper())          # bytes have .upper() too!
    conn.close()
    srv.close()

ready = threading.Event()
port_box = []
server_thread = threading.Thread(target=upper_server, args=(port_box, ready))
server_thread.start()
ready.wait()

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", port_box[0]))
client.sendall(b"make me loud")
print("Reply:", client.recv(1024).decode())
client.close()
server_thread.join()
print("Server shut down cleanly")

# Expected output:
# Reply: MAKE ME LOUD
# Server shut down cleanly

📚 Keep Learning

Frequently Asked Questions

Lesson complete — you can talk over the network!

You understand TCP sockets, can create them with socket.socket, exchange data with sendall and recv, convert strings with encode/decode, build a tiny echo server and client over 127.0.0.1, and reach for socketpair() for clean demos.

Practice quiz

What is a socket, fundamentally?

  • A function that formats strings for printing
  • A type of Python dictionary for network data
  • An endpoint for sending and receiving bytes between programs
  • A file on disk used for logging

Answer: An endpoint for sending and receiving bytes between programs. A socket is one end of a network connection — programs create sockets and send bytes back and forth.

What kind of data do sockets actually transmit?

  • bytes
  • str objects
  • JSON only
  • Python lists

Answer: bytes. Sockets move raw bytes, which is why you encode strings before sending and decode after receiving.

Which method converts a str to bytes before sending?

  • .decode()
  • .bytes()
  • .pack()
  • .encode()

Answer: .encode(). Call .encode() to turn a string into bytes for sendall; use .decode() on received bytes to get a string back.

What does socket.socketpair() return?

  • A single connected socket
  • Two sockets already connected to each other
  • A server socket and its address
  • A tuple of (host, port)

Answer: Two sockets already connected to each other. socketpair() returns two already-connected sockets with no IP, port, or server setup — ideal for demos and tests.

What is the difference between send() and sendall()?

  • sendall() keeps going until every byte is sent; send() may transmit only part
  • send() is faster but encrypts the data
  • They are identical aliases
  • sendall() works only on socketpairs

Answer: sendall() keeps going until every byte is sent; send() may transmit only part. send() may send only part of the data and returns the count; sendall() loops until all bytes are sent.

In a TCP server, which call accepts an incoming client connection?

  • connect()
  • bind()
  • accept()
  • recv()

Answer: accept(). A server bind()s, listen()s, then accept()s a connection; the client connect()s to it.

What does binding a server to port 0 do?

  • Disables the server
  • Lets the operating system pick a free port
  • Always uses port 80
  • Raises an error

Answer: Lets the operating system pick a free port. Passing port 0 tells the OS to choose any available free port, which you can read via getsockname().

Why might a single recv() return only part of a large message?

  • recv() always truncates to 8 bytes
  • The socket is broken
  • Only sendall() can fix the data
  • TCP is a byte stream, not discrete packets

Answer: TCP is a byte stream, not discrete packets. TCP delivers a continuous stream, so production code loops on recv until it has a complete message.

What does sock.sendall("hello") (a str) raise?

  • Nothing — it works fine
  • A TypeError, because a bytes-like object is required
  • A ConnectionError
  • A ValueError about encoding

Answer: A TypeError, because a bytes-like object is required. sendall needs bytes; passing a str raises TypeError. Encode it first: sendall("hello".encode()).

In socket.socket(socket.AF_INET, socket.SOCK_STREAM), what does SOCK_STREAM select?

  • A UDP datagram socket
  • A raw socket
  • A reliable TCP stream socket
  • A Unix domain socket

Answer: A reliable TCP stream socket. SOCK_STREAM with AF_INET creates a TCP socket — a reliable, ordered byte stream.

Continue this course

Frequently asked questions

What is a socket, really?

A socket is an endpoint for network communication — think of it as one end of a telephone line. Two programs (possibly on different machines) each create a socket, connect them, and then send bytes back and forth. TCP sockets give you a reliable, ordered stream of bytes, which is what most everyday networking (HTTP, email, databases) is built on.

Why do I have to encode and decode strings?

Sockets send raw bytes, not Python str objects. So you call .encode() to turn a string into bytes before sending (sendall) and .decode() to turn the received bytes back into a string after recv. UTF-8 is the default and the right choice almost always. Forgetting to encode is the single most common socket beginner error.

What's the difference between send and sendall?

send() may transmit only part of your data and returns how many bytes it actually sent, leaving you to loop and send the rest. sendall() keeps going until every byte is sent (or an error occurs). For sending a whole message in one call, sendall() is almost always what you want.

What is socketpair() and why use it for demos?

socket.socketpair() returns two sockets that are already connected to each other, with no IP address, port, server, or client setup. It's perfect for testing and self-contained demos because there's nothing to bind, no port to pick, and no race between starting a server and connecting a client — you just send on one end and recv on the other.