Skip to content

WebSocket: Code Examples (Python + Pydantic + Playwright)

1. Pydantic Models for WebSocket Messages

from pydantic import BaseModel, Field
from typing import Optional, Any
from datetime import datetime
from enum import Enum


class EventType(str, Enum):
    """All supported WebSocket event types."""

    chat_message = "chat.message"
    user_joined = "user.joined"
    user_left = "user.left"
    error = "error"
    ack = "ack"


class WSMessage(BaseModel):
    """Base WebSocket message schema with event routing."""

    event: EventType
    data: dict[str, Any]
    timestamp: datetime
    message_id: Optional[str] = None


class ChatMessage(BaseModel):
    """Payload schema for chat.message events."""

    text: str = Field(min_length=1, max_length=5000)
    user_id: str
    channel_id: str


class WSError(BaseModel):
    """Error response sent to client on invalid messages."""

    event: str = "error"
    code: int
    message: str
    details: Optional[dict] = None


class AckMessage(BaseModel):
    """Acknowledgment sent after processing a client message."""

    event: str = "ack"
    message_id: str
    status: str = "received"

Usage — Validate Incoming Message

import json

raw = '{"event": "chat.message", "data": {"text": "hi"}, "timestamp": "2026-01-01T00:00:00Z"}'
msg = WSMessage.model_validate(json.loads(raw))
print(msg.event)        # EventType.chat_message
print(msg.timestamp)    # 2026-01-01 00:00:00+00:00

Build error: WSError(code=400, message="Missing required field: user_id").model_dump_json()


2. Playwright WebSocket Tests

Playwright intercepts WebSocket frames in browser tests. Use when testing a web app over WebSocket.

import pytest
import json
from playwright.sync_api import Page, WebSocket


def test_websocket_connection(page: Page):
    """Verify WebSocket connects and server sends initial frames."""
    messages: list[dict] = []

    def handle_ws(ws: WebSocket):
        ws.on("framereceived", lambda payload: messages.append(json.loads(payload)))

    page.on("websocket", handle_ws)
    page.goto("https://app.example.com/chat")
    page.wait_for_timeout(2000)

    assert len(messages) > 0, "Expected at least one message from server"


def test_websocket_send_receive(page: Page):
    """Send a chat message via UI and validate the echoed frame."""
    received: list[dict] = []

    def handle_ws(ws: WebSocket):
        ws.on("framereceived", lambda data: received.append(json.loads(data)))

    page.on("websocket", handle_ws)
    page.goto("https://app.example.com/chat")
    page.wait_for_timeout(1000)

    page.fill("#message-input", "Hello, World!")
    page.click("#send-button")
    page.wait_for_timeout(1000)

    for msg in received:
        validated = WSMessage.model_validate(msg)
        assert validated.event in list(EventType)


def test_websocket_close(page: Page):
    """Verify WebSocket close event is captured."""
    closed = []

    def handle_ws(ws: WebSocket):
        ws.on("close", lambda: closed.append(True))

    page.on("websocket", handle_ws)
    page.goto("https://app.example.com/chat")
    page.wait_for_timeout(1000)

    page.click("#disconnect-button")
    page.wait_for_timeout(1000)

    assert len(closed) > 0, "WebSocket should have closed"

3. Direct WebSocket Testing (websockets library)

For testing WebSocket servers directly, without a browser. Uses the websockets library with asyncio.

import asyncio
import json
import pytest
import websockets


@pytest.mark.asyncio
async def test_ws_echo():
    """Connect to echo server, send message, validate response."""
    uri = "ws://localhost:8080/ws"
    async with websockets.connect(uri) as ws:
        message = {
            "event": "chat.message",
            "data": {"text": "hello", "user_id": "u1"},
            "timestamp": "2026-01-01T00:00:00Z",
            "message_id": "msg-001",
        }
        await ws.send(json.dumps(message))

        response = await asyncio.wait_for(ws.recv(), timeout=5.0)
        data = json.loads(response)

        validated = WSMessage.model_validate(data)
        assert validated.event is not None
        assert validated.message_id is not None


@pytest.mark.asyncio
async def test_ws_invalid_json():
    """Send invalid JSON, expect error response."""
    async with websockets.connect("ws://localhost:8080/ws") as ws:
        await ws.send("{broken json")

        response = await asyncio.wait_for(ws.recv(), timeout=5.0)
        data = json.loads(response)

        error = WSError.model_validate(data)
        assert error.code == 400


@pytest.mark.asyncio
async def test_ws_ack():
    """Send message with ID, expect ACK with same ID."""
    async with websockets.connect("ws://localhost:8080/ws") as ws:
        msg = {
            "event": "chat.message",
            "data": {"text": "ping"},
            "timestamp": "2026-01-01T00:00:00Z",
            "message_id": "msg-999",
        }
        await ws.send(json.dumps(msg))
        response = await asyncio.wait_for(ws.recv(), timeout=5.0)
        ack = AckMessage.model_validate(json.loads(response))
        assert ack.message_id == "msg-999"

Run: uv pip install websockets pydantic pytest pytest-asyncio && uv run pytest test_websocket.py -v