Top Stories

Python Network Programming: A Complete Practical Guide
Ankaj Gupta
June 03, 2026

Python Network Programming: A Complete Practical Guide

Python Programming

Python Network Programming: Build Real Clients & Servers

Go from zero to running your own TCP echo server, HTTP client, and async chat backend — with hands-on code you can run in two terminals today.

~35 min read Python 3.10+ 6 phases Beginner → Intermediate
Python Network Programming — clients, servers, and network protocols

6

Learning phases

15+

Runnable examples

4

Practice projects

0

Paid libs required

Quick wins — What you will learn

IP, ports & sockets (TCP vs UDP)

TCP echo server + client

Multi-client with threads & asyncio

HTTP, TLS & secure connections

Your learning path

Pro tip: For production web apps, use FastAPI or Django. Learn raw sockets first — you will understand timeouts, connection pools, and WebSockets much faster.

Before you start

You will need

  • • Python 3.10 or newer installed
  • • Basic Python (functions, classes, exceptions)
  • • Terminal access to run two processes (server + client)
  • • Optional: pip install requests for HTTP examples

Helpful tools

  • netstat / ss — see open ports
  • telnet host port or nc — quick connectivity tests
  • • Wireshark — inspect packets (advanced)

Network basics every Python developer should know

When your Python program talks to another machine, it uses a stack of protocols. You rarely touch layers 1–3 directly; most app code lives at the transport and application layers.

Protocol stack (simplified)
┌─────────────────────────────────────────┐
│  Application   HTTP · DNS · your app    │  ← you write here
├─────────────────────────────────────────┤
│  Transport     TCP  │  UDP              │  ← socket module
├─────────────────────────────────────────┤
│  Network       IP (192.168.x.x)         │
├─────────────────────────────────────────┤
│  Link          Wi‑Fi · Ethernet         │  ← OS / hardware
└─────────────────────────────────────────┘

IP address + port = endpoint

A socket is identified by (IP, port, protocol). Example: your browser connects to 142.250.80.46:443 (Google, HTTPS).

TCP Reliable
  • Connection handshake first
  • Guaranteed order & delivery
  • Retransmits lost packets

HTTP · SSH · MongoDB · most APIs

UDP Fast
  • No connection setup
  • Lower latency
  • May lose or reorder packets

DNS · gaming · video · broadcasts

How TCP connects (3-way handshake)

1. SYN

Client → Server

2. SYN-ACK

Server → Client

3. ACK

Connection ready

Ports you will see every day

:80 HTTP :443 HTTPS :22 SSH :27017 MongoDB :9999 our demos
Term Meaning
127.0.0.1 Loopback — same machine only (localhost)
0.0.0.0 Bind — listen on all network interfaces
Port 0–65535; ports < 1024 often need admin on Linux
Hostname Resolved to IP via DNS (socket.gethostbyname)

The Python socket module

The standard library socket module wraps Berkeley sockets — the same API used in C, Go, and Rust.

Server flow

socket() bind() listen() accept() recv/send

Client flow

socket() connect() send/recv
create_socket.py Python
import socket

# IPv4 + TCP (most common)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1

Phase 1 — TCP client & server

~10 min · Your first working network app

Step 1: Minimal TCP echo server

This server listens on port 9999, accepts one connection at a time, and echoes bytes back.

server.py Python
# server.py
import socket

HOST = "127.0.0.1"   # localhost only — safe for learning
PORT = 9999

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen(5)
    print(f"Listening on {HOST}:{PORT}...")

    conn, addr = server.accept()
    with conn:
        print(f"Connected by {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)  # echo back

SO_REUSEADDR: Allows rebinding immediately after restart instead of waiting for TIME_WAIT.

Step 2: TCP client

Run the server in one terminal, then run the client in another:

client.py Python
# client.py
import socket

HOST = "127.0.0.1"
PORT = 9999

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect((HOST, PORT))
    sock.sendall(b"Hello from Python client!")
    response = sock.recv(1024)

print("Server replied:", response.decode())

Try it yourself — 60 seconds

  1. Save server.py and client.py
  2. Terminal 1: python server.py
  3. Terminal 2: python client.py

Expected output

# server.py
Listening on 127.0.0.1:9999...
Connected by ('127.0.0.1', 54321)

# client.py
Server replied: Hello from Python client!

Step 3: Send and receive strings safely

Sockets work with bytes, not str. Always encode before send and decode after recv. For structured messages, prefix length or use a delimiter/newline protocol.

def send_msg(sock: socket.socket, text: str) -> None:
    sock.sendall(text.encode("utf-8"))

def recv_line(sock: socket.socket, bufsize: int = 4096) -> str:
    data = sock.recv(bufsize)
    if not data:
        raise ConnectionError("Peer closed connection")
    return data.decode("utf-8")

Important: recv(1024) may return fewer than 1024 bytes. For large payloads, loop until you have the full message or use a framing scheme.

2

Phase 2 — UDP (connectionless)

~5 min · Fire-and-forget messaging

UDP has no connect() handshake on the server side — you recvfrom() and reply with sendto().

UDP server

# udp_server.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 9998))
print("UDP server on 9998...")

while True:
    data, addr = sock.recvfrom(1024)
    print(f"From {addr}: {data.decode()}")
    sock.sendto(b"ACK: " + data, addr)

UDP client

# udp_client.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(b"Ping", ("127.0.0.1", 9998))
reply, _ = sock.recvfrom(1024)
print(reply.decode())
3

Phase 3 — Handling multiple clients

~8 min · Scale beyond one connection

A single-threaded server blocks on accept() — only one client at a time. Pick your strategy:

1

Thread per client

Simple for moderate load.

Best for: learning, <100 clients
2

select / poll

One thread, many sockets.

Best for: legacy servers
Recommended 3

asyncio

Thousands of idle connections.

Best for: chat, WebSockets

Threaded echo server

# threaded_server.py
import socket
import threading

HOST, PORT = "127.0.0.1", 9999

def handle_client(conn: socket.socket, addr):
    with conn:
        print(f"Thread {threading.current_thread().name} serving {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

def main():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((HOST, PORT))
        server.listen()
        print(f"Threaded server on {HOST}:{PORT}")
        while True:
            conn, addr = server.accept()
            t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
            t.start()

if __name__ == "__main__":
    main()
4

Phase 4 — HTTP networking

~8 min · How the web actually works

HTTP runs on top of TCP (port 80 or 443). You can craft raw HTTP by hand to learn the protocol, but production code uses higher-level libraries.

Raw HTTP over a socket (educational)

import socket

host = "example.com"
port = 80

request = (
    f"GET / HTTP/1.1\r\n"
    f"Host: {host}\r\n"
    f"Connection: close\r\n"
    f"\r\n"
).encode()

with socket.create_connection((host, port)) as sock:
    sock.sendall(request)
    chunks = []
    while True:
        part = sock.recv(4096)
        if not part:
            break
        chunks.append(part)

response = b"".join(chunks).decode("utf-8", errors="replace")
print(response[:500])  # status line + headers + start of body

urllib (standard library)

from urllib.request import urlopen
from urllib.error import URLError, HTTPError

url = "https://httpbin.org/get"

try:
    with urlopen(url, timeout=10) as resp:
        print("Status:", resp.status)
        print("Headers:", dict(resp.headers))
        body = resp.read().decode()
        print(body[:300])
except HTTPError as e:
    print("HTTP error:", e.code, e.reason)
except URLError as e:
    print("Network error:", e.reason)

requests library (recommended for apps)

pip install requests
import requests

resp = requests.get(
    "https://api.github.com/users/python",
    timeout=10,
    headers={"Accept": "application/json"},
)
resp.raise_for_status()
data = resp.json()
print(data["login"], data.get("public_repos"))
5

Phase 5 — TLS / SSL encryption

~7 min · Encrypt traffic in transit

Wrap a TCP socket with the ssl module to get HTTPS-style encryption. Never invent your own crypto — use TLS.

TLS client (connect to HTTPS server)

import socket
import ssl

hostname = "www.python.org"
port = 443

context = ssl.create_default_context()  # verifies server certificate

with socket.create_connection((hostname, port)) as sock:
    with context.wrap_socket(sock, server_hostname=hostname) as ssock:
        print("TLS version:", ssock.version())
        ssock.sendall(b"GET / HTTP/1.1\r\nHost: www.python.org\r\n\r\n")
        print(ssock.recv(2048).decode()[:400])

Production tip: Use requests or httpx for HTTPS — they handle certificates, redirects, and timeouts correctly.

TLS server (self-signed for local testing)

Generate a cert with OpenSSL, then wrap the server socket:

OpenSSL: create self-signed cert (click to expand)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -days 365 -nodes -subj "/CN=localhost"
import socket
import ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("cert.pem", "key.pem")

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.bind(("127.0.0.1", 10023))
    sock.listen(5)
    with context.wrap_socket(sock, server_side=True) as ssock:
        conn, addr = ssock.accept()
        with conn:
            data = conn.recv(1024)
            conn.sendall(b"Secure echo: " + data)
6

Phase 6 — asyncio streams (modern Python)

~8 min · High-concurrency servers

asyncio uses a single thread and an event loop. asyncio.start_server is the high-level API for TCP servers.

Async echo server

# async_server.py
import asyncio

async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
    addr = writer.get_extra_info("peername")
    print(f"Client connected: {addr}")
    while True:
        data = await reader.read(1024)
        if not data:
            break
        writer.write(data)
        await writer.drain()
    writer.close()
    await writer.wait_closed()

async def main():
    server = await asyncio.start_server(handle, "127.0.0.1", 9997)
    addrs = ", ".join(str(s.getsockname()) for s in server.sockets)
    print(f"Serving on {addrs}")
    async with server:
        await server.serve_forever()

asyncio.run(main())

Async client

# async_client.py
import asyncio

async def main():
    reader, writer = await asyncio.open_connection("127.0.0.1", 9997)
    writer.write(b"Hello asyncio!")
    await writer.drain()
    reply = await reader.read(1024)
    print("Reply:", reply.decode())
    writer.close()
    await writer.wait_closed()

asyncio.run(main())

Which Python module should I use?

socket

Raw TCP/UDP

asyncio

Async TCP servers

urllib

Simple HTTP GET

requests

Production HTTP client

ssl

TLS on sockets

httpx / aiohttp

Async HTTP

Choosing the right approach

Use case Best tool
REST API consumer requests or httpx
Custom TCP protocol socket or asyncio streams
Web app / JSON API server FastAPI, Flask, Django
DNS / discovery / gaming UDP sockets
Encrypted custom protocol ssl wrapping socket + asyncio

Self-check quiz

Click each question to reveal the answer. Be honest — if you miss one, re-read that section.

What is the difference between TCP and UDP?
TCP is connection-oriented and guarantees delivery order; UDP is connectionless and faster but may drop packets.
Why must you use bytes with sockets, not str?
Sockets transfer raw binary on the wire. Encode strings with .encode("utf-8") before send; decode after recv.
When should you use asyncio instead of threads?
When you have many connections that spend most of their time waiting (I/O-bound). asyncio handles thousands of idle clients with one thread.
What does 127.0.0.1 mean vs 0.0.0.0 when binding?
127.0.0.1 = localhost only. 0.0.0.0 = listen on all network interfaces (use with firewall care).

Common mistakes to avoid

Assuming one recv() returns a full message

Forgetting to set socket timeouts (hangs forever)

Binding to 0.0.0.0 on untrusted networks without a firewall

Mixing threads and asyncio on the same socket

Sending str instead of bytes over sockets

Disabling TLS certificate verification in production

# Always set a timeout on blocking sockets
sock.settimeout(30.0)

# Prefer context managers
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    ...

Practice projects

Level up by building these — each maps to a phase in this guide.

Easy 01

Line-based chat server

Broadcast each line to all clients. Use \n as delimiter. → Phase 3

Medium 02

Port scanner

connect_ex() on ports 1–1024 with a thread pool. → Phase 1 & 3

Medium 03

File transfer over TCP

Send 8-byte size header, then chunks + SHA-256 verify. → Phase 1

Hard 04

Async weather fetcher

Fetch 5 cities concurrently with asyncio + aiohttp. → Phase 4 & 6

Interactive learning checklist

Check off skills as you master them — your browser will remember (local only).

You're ready to go deeper

Python network programming starts with sockets: bind, listen, connect, send, and receive bytes. Layer on HTTP, TLS, and asyncio as you grow.

Next steps: build the chat server → try FastAPI → explore WebSockets. The socket mental model stays with you.

Python python network programming Python programming
Read
How to Secure AWS EC2 Ubuntu with Letโ€™s Encrypt SSL (Certbot + Nginx)
Ankaj Gupta
February 05, 2026

How to Secure AWS EC2 Ubuntu with Let’s Encrypt SSL (Certbot + Nginx)

Install Let's Encrypt SSL on AWS EC2 Ubuntu (with Certbot & Nginx)

Step-by-step guide to securing your AWS EC2 Ubuntu instance with a free Let's Encrypt TLS/SSL certificate using Certbot and Nginx, updated for modern Ubuntu.

AWS EC2 Ubuntu server secured with Let's Encrypt SSL
Published: 10–12 min read

Let's Encrypt provides free TLS/SSL certificates so your website can use HTTPS. On AWS EC2 with Ubuntu, the easiest way to get and renew these certificates is with Certbot. This guide shows you how to secure an Nginx site on Ubuntu using the modern, Snap-based Certbot installation, inspired by older tutorials but updated for today's tooling.

๐Ÿ“š What You'll Learn

  • Prerequisites for using Let's Encrypt on an AWS EC2 Ubuntu instance
  • How to install Certbot using Snap (recommended for Ubuntu)
  • How to issue and auto-configure an HTTPS certificate for Nginx
  • How automatic renewal works and how to test it

1. Prerequisites

  • ✔ An AWS EC2 instance running Ubuntu (22.04 / 20.04 or similar)
  • ✔ A registered domain name pointing to your EC2 public IP (via DNS A/AAAA record)
  • ✔ Nginx installed and serving your site on HTTP (port 80)
  • ✔ SSH access with sudo privileges
Important: Let's Encrypt does not issue certificates for raw IP addresses (e.g. 192.168.1.10). You must use a real domain (like example.com) that resolves to your server.

2. Install Certbot on Ubuntu (Snap method)

Older guides used a Certbot PPA (e.g. ppa:certbot/certbot) and packages like python-certbot-nginx. On modern Ubuntu releases, the official recommendation is to use Snap instead. We'll start with the modern Snap approach and then, in the next section, briefly cover the legacy PPA method for older Ubuntu versions.

2.1 Update packages and install Snap

sudo apt update
sudo apt install snapd -y

2.2 Install and refresh Snap core

sudo snap install core
sudo snap refresh core

2.3 Install Certbot

sudo snap install --classic certbot

Create a convenient symlink so you can run certbot directly:

sudo ln -s /snap/bin/certbot /usr/bin/certbot

3. Legacy PPA-based Certbot install (older Ubuntu)

If you're running an older Ubuntu release (for example, 16.04 or 18.04) and can't use Snap, you may still find guides that use the ppa:certbot/certbot repository. This method is deprecated but helpful to understand if you maintain legacy servers.

3.1 Add Certbot PPA and dependencies

First, connect to your EC2 Ubuntu instance via SSH, then run:

sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update

This installs the tools needed for managing PPAs, adds the Certbot repository, and refreshes your package index.

3.2 Install Certbot plugins for Apache or Nginx

Choose the package that matches your web server:

# For Apache
sudo apt-get install python-certbot-apache

# For Nginx
sudo apt-get install python-certbot-nginx

These packages install Certbot plus the appropriate plugin to automatically edit your Apache/Nginx configuration.

3.3 Issue certificates (Apache or Nginx)

Once Certbot is installed, you can request certificates for one or more domains. The first domain is treated as the primary name, additional ones are aliases:

# Apache example
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com

# Nginx example
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will store your certificate and private key under /etc/letsencrypt/live/yourdomain.com/ and automatically update your virtual host / server block configuration in /etc/apache2/sites-available/ or /etc/nginx/sites-available/.

Note: On newer Ubuntu versions, this PPA-based flow may fail or be unavailable. Prefer the Snap method in section 2 whenever possible.

4. Obtain a Let's Encrypt SSL certificate for Nginx

Make sure Nginx is serving your site on port 80 and the domain points to this server. Then run Certbot's Nginx plugin, which will obtain a certificate and update your Nginx configuration automatically.

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
  • Replace yourdomain.com and www.yourdomain.com with your real domain(s).
  • Certbot will ask for an email address and terms of service agreement.
  • You can choose whether to redirect all HTTP traffic to HTTPS (recommended).

5. Test automatic renewal

Let's Encrypt certificates are valid for 90 days. Certbot installs a systemd timer to renew them automatically. You can simulate a renewal to confirm everything is wired correctly:

sudo certbot renew --dry-run

If you see no errors, your certificates will renew automatically before they expire.

6. Common issues & fixes

E: Unable to locate package python-certbot-nginx

This usually means you're following an old PPA-based guide. Remove the PPA and use the Snap method shown above instead.

Challenge failed / HTTP-01 validation errors

Ensure port 80 is open in your AWS security group and that your domain's DNS A/AAAA record points to this EC2 instance. Certbot must be able to reach http://yourdomain.com/.well-known/ during validation.

7. Summary

With Certbot and Let's Encrypt, you can secure your AWS EC2 Ubuntu instance with HTTPS in just a few commands. Compared to older PPA-based approaches, the Snap method is more reliable on modern Ubuntu versions and keeps Certbot up to date automatically.

certbot nginx certbot ppa not found free ssl certificate nginx ssl setup ssl ubuntu ubuntu certbot ssl
Read
Manage Multiple Python Versions on Ubuntu with Pyenv
Ankaj Gupta
February 05, 2026

Manage Multiple Python Versions on Ubuntu with Pyenv

Pyenv on Ubuntu: Install, Switch, and Manage Multiple Python Versions (Safely)

A practical, copy-paste friendly guide to installing pyenv, building Python versions, and keeping system Python untouched.

Thumbnail illustration for managing multiple Python versions on Ubuntu with pyenv
Published: 10–12 min read

Ubuntu ships with a “system Python” that OS tools rely on. Replacing or altering it can break package managers and system utilities. pyenv solves this by installing additional Python versions under your home directory and switching between them via lightweight shims—so your OS stays safe while your projects stay reproducible.

๐Ÿ“š What You’ll Learn

  • How pyenv works (shims + version selection)
  • Installing pyenv on Ubuntu (recommended method)
  • Installing a specific Python version and setting it globally/locally
  • Best practices: virtual environments, upgrades, and troubleshooting

1. Why pyenv (and why not replace system Python)

On Ubuntu, the system Python may be used by OS components and package tooling. pyenv installs additional Pythons under ~/.pyenv and selects them by updating your PATH to point at pyenv’s shims first.

✅ What you get

  • Multiple Python versions side-by-side (per user, no sudo)
  • Project-specific versions via .python-version
  • Simple switching: global, local, or shell session

⚠️ What to avoid

  • Don’t remove/replace Ubuntu’s system Python packages
  • Don’t rely on sudo pip for system installs

2. Install build dependencies (Ubuntu)

pyenv builds CPython from source, so you’ll need compilers and common libraries. Run:

sudo apt update
sudo apt install -y \
  make build-essential libssl-dev zlib1g-dev \
  libbz2-dev libreadline-dev libsqlite3-dev \
  wget curl llvm libncursesw5-dev xz-utils tk-dev \
  libffi-dev liblzma-dev

If you later hit build errors (OpenSSL, zlib, bz2), revisit this step—missing system libraries are the #1 cause of pyenv install failures.

3. Install pyenv

The official installer script downloads pyenv and common plugins into ~/.pyenv. It does not automatically edit your shell files—you’ll do that in the next step.

curl -fsSL https://pyenv.run | bash

Prefer not to pipe to bash? You can also install from the official GitHub repo; see references at the end.

4. Configure your shell (Bash / Zsh)

You need two things: put pyenv on PATH, and initialize it so the shims work. Add the snippet below to the appropriate file(s) for your shell.

Bash (Ubuntu default)

Add to ~/.bashrc:

export PYENV_ROOT="$HOME/.pyenv"
[[ -d "$PYENV_ROOT/bin" ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"

For login shells, also ensure PATH is set early (often ~/.profile on Ubuntu):

export PYENV_ROOT="$HOME/.pyenv"
[[ -d "$PYENV_ROOT/bin" ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"

Zsh

Add to ~/.zshrc:

export PYENV_ROOT="$HOME/.pyenv"
[[ -d "$PYENV_ROOT/bin" ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"

Restart your terminal (or run source ~/.bashrc) and verify:

pyenv --version
pyenv root

5. Install a Python version (example: Python 3.12)

First, list available versions and pick the exact patch release you want:

pyenv install --list | grep -E "^\s*3\.12\."

Then install one of the listed versions (replace 3.12.x with a real value from your list):

pyenv install 3.12.x

6. Switch Python versions (global / local / shell)

๐ŸŒ Global

Default for your user account.

pyenv global 3.12.x
python --version

๐Ÿ“Œ Local (per project)

Writes .python-version in the folder.

cd your/project
pyenv local 3.12.x

⏱️ Shell (this terminal only)

Temporary override for the current session.

pyenv shell 3.12.x

Use pyenv versions to see what’s installed and what’s active.

7. Best practice: use virtual environments

pyenv selects the Python interpreter. For project dependencies, still use an isolated environment:

python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip

๐Ÿง  Tip: keep it predictable

Commit .python-version (pyenv) and a dependency lock strategy (requirements.txt, poetry.lock, etc.) so teammates and CI use the same interpreter and packages.

8. Troubleshooting common pyenv install issues

“python-build: error: … OpenSSL / zlib / bz2 …”

Re-check the Ubuntu build dependencies from Step 2. Missing libssl-dev, zlib1g-dev, or libbz2-dev is very common.

“pyenv: command not found”

Your shell isn’t loading pyenv yet. Confirm you added the init snippet to the right file, then restart the terminal. Also run echo $SHELL to confirm whether you’re using bash or zsh.

References

pyenv Python ubuntu
Read