How to Implement WebSockets in Python

This article provides an overview of how Python facilitates real-time, bidirectional communication using WebSockets. It explores the technical foundations of the WebSocket protocol within the Python ecosystem, compares the lightweight websockets library with the full-featured Django channels framework, and walks through implementation examples for building both standalone services and integrated web applications.


Understanding WebSockets in Python

The WebSocket protocol enables persistent, low-latency, full-duplex communication between a client and a server over a single TCP connection. Unlike traditional HTTP request-response cycles, WebSockets allow either party to send data independently at any time.

In Python, WebSockets are primarily powered by asynchronous I/O (asyncio) or the Asynchronous Server Gateway Interface (ASGI) specification, allowing servers to handle thousands of concurrent open connections without thread-blocking overhead.


Implementation with the websockets Library

The websockets library is a lightweight, high-performance package built directly on Python’s native asyncio module. It is best suited for standalone microservices, real-time data feeds, and applications that do not require an extensive web framework.

1. Building a Server

The library provides an asynchronous context manager and event loop integration to manage client handshakes and message streaming:

import asyncio
import websockets

async def echo_handler(websocket):
    async for message in websocket:
        print(f"Received message: {message}")
        await websocket.send(f"Echo: {message}")

async def main():
    async with websockets.serve(echo_handler, "localhost", 8765):
        print("WebSocket server running on ws://localhost:8765")
        await asyncio.Future()  # Keeps the server running

if __name__ == "__main__":
    asyncio.run(main())

2. Building a Client

Connecting to a server follows an identical asynchronous design pattern:

import asyncio
import websockets

async def send_message():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as websocket:
        await websocket.send("Hello, Server!")
        response = await websocket.recv()
        print(f"Server response: {response}")

if __name__ == "__main__":
    asyncio.run(send_message())

In this model, the event loop handles context switching automatically whenever an operation waits for network I/O, ensuring high throughput with minimal resource consumption.


Implementation with Django Channels

Django channels extends Django to handle asynchronous protocols like WebSockets alongside standard HTTP. It adopts the ASGI standard, decoupling protocol decoding from application logic and integrating seamlessly with Django’s session and authentication systems.

1. Defining Consumers

A Consumer in Channels is the equivalent of a Django view, but designed for persistent connections. The AsyncWebsocketConsumer handles connection lifecycles and messaging events:

import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_name = "general"
        self.room_group_name = f"chat_{self.room_name}"

        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.accept()

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

    async def receive(self, text_data):
        data = json.loads(text_data)
        message = data.get("message", "")

        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                "type": "chat_message",
                "message": message
            }
        )

    async def chat_message(self, event):
        message = event["message"]
        # Send message to WebSocket
        await self.send(text_data=json.dumps({"message": message}))

2. Routing and Channel Layers

Channels routes incoming WebSocket requests through an ASGI routing tree rather than standard urls.py:

from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r"ws/chat/$", consumers.ChatConsumer.as_asgi()),
]

To coordinate communication across distributed workers or server instances, Channels uses a Channel Layer (commonly backed by Redis). This acts as a message broker, allowing messages published on one process to broadcast across all active consumer instances listening to a specific group.


Choosing Between websockets and channels