Python Central HubSign in

Python Projects / Intermediate

Basic Chatroom App

Build a multi-client chatroom in Python with sockets and threads — a server that broadcasts messages to every connected client — then harden it with usernames, clean disconnects, message framing, and a path to a GUI.

Abstract

A chatroom is the "hello world" of network programming — the moment sockets stop being abstract and start feeling like magic. In this tutorial you build a TCP server that accepts many clients at once and broadcasts each message to everyone else, plus a client that sends and receives at the same time. You'll meet the two ideas that power almost all networked apps: sockets (the pipe between machines) and threads (so one slow client doesn't block the rest). Then you'll harden the naive version with usernames, graceful disconnects, and proper message framing.

You will leave understanding:

  • The TCP server lifecycle: socket → bind → listen → accept.
  • Why each client needs its own thread, and what broadcast does.
  • How encode/decode move strings across the wire as bytes.
  • Why bare except: and unframed recv are the two classic chat bugs.

Prerequisites

  • Python 3.6 or above.
  • A text editor or IDE.
  • Standard library only — socket and threading are built in.
  • A basic mental model of client/server networking.

Getting Started

Create the project

  1. Create a folder named chatroom.
  2. Inside it, create basic_chatroom_app.py.

Write the code

basic_chatroom_app.pySource
basic_chatroom_app.py
"""
Basic Chatroom App

A Python application that allows multiple users to communicate in a chatroom.
Features include:
- Server-side implementation to manage multiple clients.
- Client-side implementation for sending and receiving messages.
"""

import socket
import threading


def ask(prompt="", default=""):
    """Read a line, or fall back to `default` when nobody is there to type.

    Without this the script raises EOFError the moment it runs unattended — in
    a test, a scheduled job, or the build that captures this output for the
    docs. The fallback is printed rather than silent, so a reader can always
    tell which answers were typed and which were assumed.
    """
    try:
        return input(prompt).strip() or default
    except EOFError:
        print(f"{default}   (no input available, using the default)")
        return default

# Server-side implementation
class ChatServer:
    def __init__(self, host="localhost", port=12345):
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.bind((host, port))
        self.server.listen(5)
        print(f"Server started on {host}:{port}")

        self.clients = []

    def broadcast(self, message, client_socket):
        """Send a message to all connected clients except the sender."""
        for client in self.clients:
            if client != client_socket:
                try:
                    client.send(message)
                except:
                    self.clients.remove(client)

    def handle_client(self, client_socket):
        """Handle communication with a connected client."""
        while True:
            try:
                message = client_socket.recv(1024)
                if message:
                    print(f"Received: {message.decode('utf-8')}")
                    self.broadcast(message, client_socket)
            except:
                self.clients.remove(client_socket)
                client_socket.close()
                break

    def run(self):
        """Start the server and accept incoming connections."""
        while True:
            client_socket, client_address = self.server.accept()
            print(f"New connection: {client_address}")
            self.clients.append(client_socket)

            threading.Thread(target=self.handle_client, args=(client_socket,)).start()


# Client-side implementation
class ChatClient:
    def __init__(self, host="localhost", port=12345):
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.connect((host, port))

        threading.Thread(target=self.receive_messages).start()

    def send_message(self, message):
        """Send a message to the server."""
        self.client.send(message.encode("utf-8"))

    def receive_messages(self):
        """Receive messages from the server."""
        while True:
            try:
                message = self.client.recv(1024).decode("utf-8")
                print(message)
            except:
                print("Disconnected from server.")
                self.client.close()
                break


if __name__ == "__main__":
    choice = ask("Do you want to start the server or client? (server/client): ", '1').strip().lower()

    if choice == "server":
        server = ChatServer()
        server.run()
    elif choice == "client":
        client = ChatClient()
        print("Type your messages below:")
        while True:
            msg = ask("", '1')
            client.send_message(msg)
    else:
        print("Invalid choice. Exiting.")

Run it

cmd
# Terminal 1 — start the server
C:\Users\Your Name\chatroom> python basic_chatroom_app.py
Do you want to start the server or client? (server/client): server
 
# Terminal 2 and 3 — start clients
C:\Users\Your Name\chatroom> python basic_chatroom_app.py
Do you want to start the server or client? (server/client): client

Type in one client; the message appears in the others.

How it fits together

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid

Step-by-Step Explanation

1. The server socket

Python
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.server.listen(5)

AF_INET = IPv4, SOCK_STREAM = TCP (reliable, ordered). bind claims the address/port, listen(5) opens the door with a backlog of 5 pending connections.

2. Accepting clients, one thread each

Python
while True:
    client_socket, client_address = self.server.accept()   # blocks until someone connects
    self.clients.append(client_socket)
    threading.Thread(target=self.handle_client, args=(client_socket,)).start()

accept() blocks until a client connects, then returns a dedicated socket for that client. Spinning up a thread per client means the server can talk to everyone concurrently — without threads, the second client would wait for the first to finish.

3. Broadcasting

Python
def broadcast(self, message, client_socket):
    for client in self.clients:
        if client != client_socket:       # don't echo back to the sender
            try:
                client.send(message)
            except:
                self.clients.remove(client)

The server is a relay: a message from one client goes out to every other client. That if client != client_socket is what stops you from seeing your own messages twice.

4. Bytes on the wire

Python
self.client.send(message.encode("utf-8"))      # str -> bytes to send
message = self.client.recv(1024).decode("utf-8")  # bytes -> str on receive

Sockets move bytes, not strings. encode/decode translate. recv(1024) reads up to 1024 bytes at a time.

Fix #1: Stop Swallowing Errors

Bare except: hides bugs (it even catches Ctrl-C). Catch what you mean and modifying a list while iterating it is unsafe:

Python
def broadcast(self, message, sender):
    dead = []
    for client in self.clients:
        if client is sender:
            continue
        try:
            client.send(message)
        except OSError:           # socket actually broken
            dead.append(client)
    for client in dead:           # remove AFTER the loop
        self.clients.remove(client)

Fix #2: Add Usernames

Right now everyone's anonymous. Have each client send a name on join, store it, and prefix messages:

Python
# client, right after connect:
self.client.send(self.username.encode("utf-8"))
 
# server, first recv in handle_client:
name = client_socket.recv(1024).decode("utf-8")
self.names[client_socket] = name
self.broadcast(f"*** {name} joined ***".encode(), client_socket)

Fix #3: Frame Your Messages

TCP is a stream, not a series of messages — two fast sends can arrive glued together, or one send can arrive split. For real apps, prefix each message with its length:

Python
import struct
 
def send_msg(sock, text):
    data = text.encode("utf-8")
    sock.sendall(struct.pack(">I", len(data)) + data)   # 4-byte length header
 
def recv_exact(sock, n):
    buf = b""
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            return None
        buf += chunk
    return buf

Read the 4-byte length, then read exactly that many bytes. This is how you avoid the "messages mash together" bug.

Common Mistakes

ProblemCauseFix
Address already in usePrevious server didn't release the portsetsockopt(SO_REUSEADDR) before bind
Messages glue together / splitTCP is a stream, not messagesLength-prefix framing
Server crashes when a client quitsBare except + list mutation in loopCatch OSError, remove after the loop
Client can't type and receiveReceive loop on main threadRun recv on its own thread (the code does)
Garbled non-ASCII textWrong/missing encodingAlways encode/decode("utf-8")
Everyone is "anonymous"No handshakeSend a username on connect

Variations to Try

  1. Private messages — /msg alice hello routed to one user.
  2. Chat rooms — multiple named channels; clients join/leave.
  3. Tkinter GUI — replace input()/print() with a window (see Chat Application with Sockets).
  4. Message history — persist the last N messages and replay on join.
  5. Nicknames & colors — per-user display styling.
  6. asyncio rewrite — swap threads for async/await.
  7. Encryption — wrap the socket in TLS with ssl.

Real-World Applications

  • Messaging apps — the conceptual core of Slack, Discord, IRC.
  • Live support widgets — customer chat on websites.
  • Multiplayer games — real-time state sync between players.
  • IoT command channels — devices reporting to a hub.

Educational Value

  • Socket programming — the server lifecycle and TCP basics.
  • Concurrency — thread-per-client and shared state.
  • Serialization — encoding text to bytes and message framing.
  • Robustness — handling the disconnect/error paths that demos ignore.

Next Steps

  • Replace bare except with targeted error handling.
  • Add usernames and join/leave notices.
  • Implement length-prefixed framing.
  • Build a Tkinter GUI client.

Checkpoint exercise

This checkpoint practices the chat protocol: parse slash commands from raw lines and render each event for display.

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Conclusion

You built a multi-client chatroom and met the two pillars of network programming — sockets and threads — then fixed the three bugs every first chat app has: swallowed errors, anonymous users, and unframed streams. From here, a GUI or asyncio rewrite is a natural next step, and the relay-server pattern you learned underpins every messaging system you've ever used. Full source on GitHub. Explore more networking projects on Python Central Hub.

Was this page helpful?

Let us know how we did