Build a Multi-Client Chat Room

Build a TCP chat room in Python using sockets, threading, and concurrency.

Introduction

30 Second Summary

Every time you send a message on Slack, Discord, or a multiplayer game, your computer opens a direct connection to a server and streams data back and forth in real-time. That connection is called a socket.

In this project, you will build a multi-client chat room from scratch using Python's built-in networking tools. You'll create both a server that manages connections and a client that sends and receives messages in real-time.

What You'll Build

A terminal-based chat system where multiple users connect simultaneously and exchange messages through TCP sockets and threading.

By the end of this project, you'll have:

  • A TCP chat server that accepts multiple simultaneous connections and broadcasts messages to all connected users.
  • A chat client that sends and receives messages concurrently using separate threads.
  • Hands-on understanding of the socket lifecycle that powers every networked application.
  • Secret Mission: Add private messaging so users can whisper directly to specific users with a /whisper command.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, loops, and if-statements). No networking experience is required.

Build the Chat Server Foundation

Your environment is set up and ready to go. Now it's time to build the core of your chat room: the server.

The server's job is to listen for incoming connections, keep track of who's online, and relay messages between clients. You'll use Python's socket module for TCP networking and threading to handle multiple clients at the same time.

In this step, get ready to:
  • Create a TCP server socket that listens for connections on port 9999.
  • Build a broadcast function that relays messages to all connected clients.
  • Write an accept loop that spawns a new thread for each connecting client.
Set up the server socket

Every networked application starts the same way: creating a socket, binding it to an address, and telling it to listen. This is the lifecycle that powers everything from web servers to multiplayer games.

  • Open server.py in VS Code and paste the following code:
import socket
import threading

HOST = 'localhost'
PORT = 9999


def main():
    # Create a TCP server socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Allow reuse of the port if the server restarts quickly
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # Bind to localhost on port 9999
    server.bind((HOST, PORT))
    # Start listening for incoming connections
    server.listen()
    print(f"Chat server listening on {HOST}:{PORT}")


if __name__ == '__main__':
    main()

What does this code do?

socket.socket(socket.AF_INET, socket.SOCK_STREAM) creates a TCP socket. AF_INET means IPv4 addressing, and SOCK_STREAM means TCP (reliable, ordered delivery).

setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tells the operating system to let you reuse the port immediately if you restart the server. Without this, you'd get an "Address already in use" error for a few minutes after stopping.

bind() attaches the socket to a specific address and port. listen() then puts the socket into server mode, ready to accept incoming connections.

  • Save server.py.
  • Test it by running this command in your terminal:
python3 server.py

You should see Chat server listening on localhost:9999 printed, and then the program exits immediately. That's expected because there's no loop yet to keep it running.

Seeing "Address already in use"?

This means port 9999 is still held by a previous process. Wait 30 seconds and try again, or check for leftover processes with lsof -i :9999 (macOS/Linux).

If the error persists, ask for help debugging the port conflict.

Add client tracking and the broadcast function

The server needs to know who's connected so it can relay messages to everyone. A dictionary maps each socket connection to a username, and a lock keeps it safe when multiple threads access it at once.

  • In server.py, find the line PORT = 9999. Add the following code below it (above def main():):
clients = {}
clients_lock = threading.Lock()


def broadcast(message, sender_conn=None):
    """Send a message to all connected clients except the sender."""
    with clients_lock:
        for conn in list(clients.keys()):
            if conn != sender_conn:
                try:
                    conn.sendall(message.encode('utf-8'))
                except OSError:
                    pass

What does this code do?

clients is a dictionary that will map each connected socket to a username. clients_lock is a threading lock that prevents race conditions when multiple threads read or modify the dictionary at the same time.

The broadcast() function loops through every connected client and sends the message to each one (except the original sender). It uses sendall() to guarantee the entire message is transmitted, and wraps the send in a try/except so a broken connection doesn't crash the whole server.

Why use a lock?

When multiple threads run at the same time, they can all try to read or modify the clients dictionary simultaneously. Without a lock, one thread might remove a client while another is mid-broadcast, causing a crash.

The with clients_lock: pattern ensures only one thread can access the dictionary at a time. Think of it like a door that only one person can pass through.

  • Save server.py.

You won't see a change yet. The broadcast function is ready but nothing calls it until clients connect.

Write the connection accept loop

Right now the server prints its message and exits. You need a loop that waits for clients to connect and spawns a dedicated thread for each one.

  • In server.py, find the print(f"Chat server listening on {HOST}:{PORT}") line inside main(). Add the following code directly below it:
    try:
        while True:
            # Wait for a new client to connect
            conn, addr = server.accept()
            # Spawn a dedicated thread for this client
            thread = threading.Thread(
                target=handle_client, args=(conn, addr), daemon=True
            )
            thread.start()
    except KeyboardInterrupt:
        print("\nServer shutting down.")
    finally:
        server.close()

What does this code do?

server.accept() blocks (waits) until a new client connects. When one does, it returns a new socket (conn) for that specific client and their address (addr).

Each client gets its own daemon thread targeting handle_client. Daemon threads automatically shut down when the main program exits, so you can stop the server cleanly with Ctrl+C.

The try/except KeyboardInterrupt catches Ctrl+C and closes the server socket gracefully instead of crashing with a traceback.

handle_client is not defined yet

You'll write the handle_client function in the next step. The server will start fine because Python only looks up the function name when a client actually connects and the thread starts.

  • Save server.py.
  • Run the server again:
python3 server.py

You should see Chat server listening on localhost:9999 printed, and this time the program stays running. It's waiting for clients to connect.

  • Press Ctrl+C to stop the server. You should see Server shutting down. printed.

Server exits immediately instead of waiting?

Make sure the try: block is indented inside main() and placed after the print() line. If it's outside main(), Python won't run it.

Also check that while True: is inside the try: block. Without the infinite loop, the server processes zero connections and falls through to finally: immediately.

If you're stuck, help me figure out why my server exits immediately.

✔️ Awesome, I've got everything!

Great. Double check you've saved server.py and confirmed the server stays running until you press Ctrl+C.

ⓧ I'd like to double check the full code

Here's the complete server.py so far:

import socket
import threading

HOST = 'localhost'
PORT = 9999

clients = {}
clients_lock = threading.Lock()


def broadcast(message, sender_conn=None):
    """Send a message to all connected clients except the sender."""
    with clients_lock:
        for conn in list(clients.keys()):
            if conn != sender_conn:
                try:
                    conn.sendall(message.encode('utf-8'))
                except OSError:
                    pass


def main():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen()
    print(f"Chat server listening on {HOST}:{PORT}")

    try:
        while True:
            conn, addr = server.accept()
            thread = threading.Thread(
                target=handle_client, args=(conn, addr), daemon=True
            )
            thread.start()
    except KeyboardInterrupt:
        print("\nServer shutting down.")
    finally:
        server.close()


if __name__ == '__main__':
    main()

Your server foundation is complete. It can listen for connections, track clients, and broadcast messages. Next up, you'll write the handle_client function that manages what happens when someone actually connects.

Handle Client Connections on the Server

Your server has a solid foundation. It can accept incoming connections and spin up a thread for each one. But those threads call handle_client, a function that doesn't exist yet.

In this step, you'll write handle_client to manage each client's full lifecycle. From receiving their username when they first connect, to relaying their messages to everyone else, to cleaning up when they disconnect.

In this step, get ready to:
  • Receive and register each client's username.
  • Relay messages between all connected clients.
  • Handle disconnections and errors gracefully.
Receive and register usernames

When a client first connects, the server needs to know who they are. The convention for this chat room is simple: the very first message a client sends is their username.

  • In server.py, find the line def main(): and add the following function above it:
def handle_client(conn, addr):
    """Handle a single client connection."""
    print(f"New connection from {addr}")
    try:
        # First message is the username
        username = conn.recv(1024).decode('utf-8').strip()
        if not username:
            conn.close()
            return

        with clients_lock:
            clients[conn] = username

        print(f"{username} joined the chat")
        broadcast(f"[Server] {username} has joined the chat!\n", sender_conn=conn)

What does this code do?

  • conn.recv(1024).decode('utf-8').strip() waits for the client to send their username, reads up to 1024 bytes, and converts it from raw bytes to a clean string.
  • The with clients_lock: block ensures only one thread modifies the clients dictionary at a time. This prevents race conditions when multiple clients connect simultaneously.
  • broadcast() sends the join announcement to every connected client except the one who just joined (so they don't see their own join message).

You won't see a change yet. The function isn't complete until you add the message loop and error handling in the next two chunks.

Watch your indentation

Python uses indentation to define code structure. Make sure the code inside try: is indented with 8 spaces (two levels in from the function definition). If your editor uses tabs, configure it to insert 4 spaces per tab.

help me fix indentation errors in my handle_client function

Add the message loop

Once a client is registered, the server needs to continuously listen for their messages and relay them to everyone else. This happens in a while True loop that only breaks when the client disconnects.

  • Still inside the try: block in handle_client, add the message loop below the broadcast() join announcement:
        # Message loop
        while True:
            data = conn.recv(1024)
            if not data:
                break
            message = data.decode('utf-8').strip()
            if not message:
                continue
            formatted = f"[{username}] {message}\n"
            print(formatted.strip())
            broadcast(formatted, sender_conn=conn)

What does this code do?

  • conn.recv(1024) blocks (waits) until the client sends data. If it returns empty bytes, the client has disconnected.
  • if not data: break exits the loop when the connection closes. The Python Socket HOWTO confirms: "When recv returns 0 bytes, it means the other side has closed the connection."
  • The message is formatted as [username] message\n so recipients know who sent it. The newline acts as a message delimiter.

You won't see a change yet. The next chunk adds error handling to complete the function.

Why newline-delimited messages?

TCP is a byte stream. It has no built-in concept of "messages." The Python Socket HOWTO states that messages must be delimited, fixed-length, or self-describing. Adding \n to the end of every message is the simplest delimiter for text chat.

Add error handling and cleanup

Network connections can drop at any time. A client might close their terminal, lose their network, or crash. Without error handling, these surprise disconnections would crash the server thread and leave stale entries in the clients dictionary.

  • Add the except and finally blocks at the same indentation level as try: (4 spaces from the left):
    except (OSError, ConnectionResetError):
        pass
    finally:
        with clients_lock:
            if conn in clients:
                username = clients.pop(conn)
                print(f"{username} left the chat")
                broadcast(f"[Server] {username} has left the chat.\n")
        conn.close()

What does this code do?

  • except (OSError, ConnectionResetError) catches network errors that happen when a client disconnects unexpectedly. The pass means "do nothing" because the cleanup happens in finally regardless.
  • The finally block runs whether the loop ended normally or via an exception. It removes the client from the dictionary, broadcasts a leave message, and closes the socket.
  • with clients_lock: protects the dictionary modification. Without the lock, two threads removing clients simultaneously could corrupt the data.
  • Save server.py.
  • Run the server to confirm your syntax is correct by running this command in Terminal:
python3 server.py
  • You should see Chat server listening on localhost:9999 printed in the terminal with no errors.
  • Press Ctrl+C to stop the server.

Seeing a NameError or IndentationError?

  • If you see IndentationError, check that your except and finally lines align with the try: line (both at 4 spaces from the left edge of the function).
  • If you see SyntaxError, make sure you haven't accidentally placed code between the except and finally blocks at the wrong indentation level.
  • If you see NameError: name 'handle_client' is not defined, your function definition must appear above def main(): in the file.

Help me fix my handle_client function errors

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete server.py file at this point:

import socket
import threading

HOST = 'localhost'
PORT = 9999

clients = {}
clients_lock = threading.Lock()


def broadcast(message, sender_conn=None):
    """Send a message to all connected clients except the sender."""
    with clients_lock:
        for conn in list(clients.keys()):
            if conn != sender_conn:
                try:
                    conn.sendall(message.encode('utf-8'))
                except OSError:
                    pass


def handle_client(conn, addr):
    """Handle a single client connection."""
    print(f"New connection from {addr}")
    try:
        # First message is the username
        username = conn.recv(1024).decode('utf-8').strip()
        if not username:
            conn.close()
            return

        with clients_lock:
            clients[conn] = username

        print(f"{username} joined the chat")
        broadcast(f"[Server] {username} has joined the chat!\n", sender_conn=conn)

        # Message loop
        while True:
            data = conn.recv(1024)
            if not data:
                break
            message = data.decode('utf-8').strip()
            if not message:
                continue
            formatted = f"[{username}] {message}\n"
            print(formatted.strip())
            broadcast(formatted, sender_conn=conn)

    except (OSError, ConnectionResetError):
        pass
    finally:
        with clients_lock:
            if conn in clients:
                username = clients.pop(conn)
                print(f"{username} left the chat")
                broadcast(f"[Server] {username} has left the chat.\n")
        conn.close()


def main():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen()
    print(f"Chat server listening on {HOST}:{PORT}")

    try:
        while True:
            conn, addr = server.accept()
            thread = threading.Thread(
                target=handle_client, args=(conn, addr), daemon=True
            )
            thread.start()
    except KeyboardInterrupt:
        print("\nServer shutting down.")
    finally:
        server.close()


if __name__ == '__main__':
    main()

What is thread safety?

When multiple threads run at the same time, they can all access the same variables. If two threads try to modify the clients dictionary at the exact same moment, the data could become corrupted.

A Lock solves this by allowing only one thread through at a time. The with clients_lock: pattern automatically acquires the lock, runs your code, then releases the lock when done.

Your server can now accept clients, register their usernames, relay messages, and clean up after disconnections. Next up, you'll build the client that actually connects to this server and lets users send messages.

Build the Chat Client

Your server is complete and ready to accept connections. Now you need something to connect to it.

The client has a unique challenge compared to the server. It needs to do two things at the same time: listen for incoming messages from other users AND let you type new messages to send. A single-threaded program can only do one of those at a time, so you will use a background thread for receiving while the main thread handles sending.

In this step, get ready to:
  • Connect to the server and send a username.
  • Receive messages from other users in real-time.
  • Build a send loop for typing and sending messages.
Set up the client connection

Before the client can send or receive chat messages, it needs to establish a TCP connection to the server. This involves creating a socket, connecting to the server's address, and identifying yourself with a username.

  • In VS Code, open client.py and add the following code:
import socket
import threading
import sys

HOST = 'localhost'
PORT = 9999


def main():
    username = input("Enter your username: ").strip()
    if not username:
        print("Username cannot be empty.")
        sys.exit(1)

    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        client.connect((HOST, PORT))
    except ConnectionRefusedError:
        print("Could not connect to server. Is it running?")
        sys.exit(1)

    # Send username as the first message
    client.sendall((username + '\n').encode('utf-8'))


if __name__ == '__main__':
    main()

What does this code do?

  • input("Enter your username: ") prompts the user for a name, and .strip() removes any accidental whitespace.
  • client.connect((HOST, PORT)) establishes a TCP connection to the server. If the server is not running, ConnectionRefusedError is raised and the client exits cleanly.
  • client.sendall() sends the username as the very first message so the server knows who just connected. The \n newline acts as a message delimiter.
  • Save client.py.

You won't test this yet because the client exits immediately after sending the username. The next two chunks add receiving and sending to make it interactive.

Write the receive function

The client needs to constantly listen for messages arriving from the server. This receive function runs in a background thread so it does not block the main thread where the user types.

  • In client.py, add the following function between the PORT = 9999 line and the def main(): line:
def receive_messages(sock):
    """Receive and display messages from the server."""
    while True:
        try:
            data = sock.recv(1024)
            if not data:
                print("\n[Disconnected from server]")
                break
            print(data.decode('utf-8'), end='')
        except OSError:
            break

What does this code do?

  • sock.recv(1024) blocks until data arrives from the server. When another user sends a message, the server broadcasts it and this line picks it up.
  • If recv returns empty bytes, the server has closed the connection. The function prints a disconnect notice and stops.
  • The except OSError handles the case where the socket is closed from the main thread (e.g. when you type /quit).
  • Save client.py.

You won't see a change yet. The next chunk wires this function into a thread and adds the send loop.

Add the send loop and start the receive thread

Now you need to start receive_messages in a daemon thread and write the loop where the user types messages. The daemon thread exits automatically when the main thread finishes, so you do not need to manually stop it.

  • In client.py, find this line inside main():
    # Send username as the first message
    client.sendall((username + '\n').encode('utf-8'))
  • Add the following code directly below it (still inside main(), before the if __name__ guard):
    # Start receiving messages in a background thread
    recv_thread = threading.Thread(target=receive_messages, args=(client,), daemon=True)
    recv_thread.start()

    print(f"Connected as '{username}'. Type messages and press Enter. Type /quit to exit.")

    try:
        while True:
            message = input()
            if message.lower() == '/quit':
                break
            if message:
                client.sendall((message + '\n').encode('utf-8'))
    except (EOFError, KeyboardInterrupt):
        pass
    finally:
        client.close()
        print("Disconnected.")

What does this code do?

  • threading.Thread(..., daemon=True) starts receive_messages in the background. The daemon=True flag means it will not prevent the program from exiting.
  • The while True loop reads user input and sends each message to the server with a newline delimiter.
  • Typing /quit breaks the loop. The finally block ensures the socket is always closed, even if the user presses Ctrl+C.
  • Save client.py.
  • To verify the client starts correctly, make sure your server is running in one terminal (python3 server.py), then open a new terminal tab and run:
python3 client.py

You should see the prompt Enter your username:. Type a name and press Enter. You should then see the connected message with your username.

Seeing "Could not connect to server"?

  • Make sure server.py is running in another terminal tab. You should see Chat server listening on localhost:9999 printed there.
  • Check that your HOST and PORT values in client.py match the ones in server.py (localhost and 9999).

Still stuck? Help me debug why my client cannot connect to the server.

✔️ Awesome, I've got everything!

Great. Double check you have saved client.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete client.py file. Compare yours against it:

import socket
import threading
import sys

HOST = 'localhost'
PORT = 9999


def receive_messages(sock):
    """Receive and display messages from the server."""
    while True:
        try:
            data = sock.recv(1024)
            if not data:
                print("\n[Disconnected from server]")
                break
            print(data.decode('utf-8'), end='')
        except OSError:
            break


def main():
    username = input("Enter your username: ").strip()
    if not username:
        print("Username cannot be empty.")
        sys.exit(1)

    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        client.connect((HOST, PORT))
    except ConnectionRefusedError:
        print("Could not connect to server. Is it running?")
        sys.exit(1)

    # Send username as the first message
    client.sendall((username + '\n').encode('utf-8'))

    # Start receiving messages in a background thread
    recv_thread = threading.Thread(target=receive_messages, args=(client,), daemon=True)
    recv_thread.start()

    print(f"Connected as '{username}'. Type messages and press Enter. Type /quit to exit.")

    try:
        while True:
            message = input()
            if message.lower() == '/quit':
                break
            if message:
                client.sendall((message + '\n').encode('utf-8'))
    except (EOFError, KeyboardInterrupt):
        pass
    finally:
        client.close()
        print("Disconnected.")


if __name__ == '__main__':
    main()

Your client is ready. Next up, you will open multiple terminals and test the full chat room with multiple users talking to each other in real-time.

Test Your Chat Room

Your server and client code is complete. Now it's time to bring everything to life and watch your chat room in action.

You'll run the server in one terminal, then connect multiple clients from separate terminals to simulate a real multi-user conversation. This is the moment where all those sockets, threads, and broadcast functions come together.

In this step, get ready to:
  • Start the chat server and connect your first client.
  • Connect a second client and exchange messages in real-time.
  • Test edge cases like disconnections and reconnections.
Start the server and connect your first client

Testing a chat room requires multiple terminal sessions running at the same time. One terminal runs the server, and each additional terminal represents a different user connecting as a client.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Confirm the terminal is inside your chat-room folder. You should see chat-room in the terminal path.
  • Start the server by running this command:
python3 server.py

You should see this output in your terminal:

Chat server listening on localhost:9999

What does this mean?

The server has created a TCP socket, bound it to port 9999, and is now waiting for clients to connect. This terminal will stay occupied by the server process.

Seeing 'Address already in use'?

This means port 9999 is still occupied by a previous run. Wait a few seconds and try again. The SO_REUSEADDR option in your code should prevent this in most cases, but the operating system sometimes needs a moment to release the port.

help me fix an Address already in use error when starting my Python socket server

Now you need a second terminal to run a client. The server terminal is busy listening for connections, so you cannot type commands there anymore.

  • Open a new terminal tab by clicking the + button in the terminal panel, or press Ctrl+Shift+` (macOS and Windows).
  • Connect as the first user by running this command:
python3 client.py
  • When prompted, type a username (for example, Alice) and press Enter.

You should see a confirmation message like this:

Connected as 'Alice'. Type messages and press Enter. Type /quit to exit.
  • Switch back to your server terminal tab. You should see a new connection logged and a join announcement printed.

Seeing 'Could not connect to server'?

Make sure your server is still running in the first terminal tab. If you accidentally closed it or pressed Ctrl+C in the wrong tab, restart it with python3 server.py before running the client.

help me troubleshoot why my Python client cannot connect to my chat server

Connect a second client and exchange messages

One client proves connectivity works. Two clients prove your broadcast logic works. Let's add another user to the chat room.

  • Open a third terminal tab by clicking the + button in the terminal panel again.
  • Connect as a second user by running this command:
python3 client.py
  • When prompted, enter a different username (for example, Bob) and press Enter.

Check your first client's terminal. You should see a join announcement:

[Server] Bob has joined the chat!
  • In the Alice client terminal, type Hello Bob! and press Enter.
  • Switch to the Bob client terminal. You should see:
[Alice] Hello Bob!
  • In the Bob client terminal, type Hey Alice, nice to meet you! and press Enter.
  • Switch to the Alice client terminal. You should see Bob's message displayed in real-time.

What is happening under the hood?

When Alice sends a message, her client encodes it and sends it over the TCP socket to the server. The server's handle_client thread receives it, formats it with Alice's username, and calls broadcast(). The broadcast function iterates through every connected client except Alice and sends the message to each one.

Each client has its own receive_messages thread running in the background. That thread is always listening for incoming data. When it arrives, it prints immediately. This is why you see messages appear without pressing anything.

Test edge cases

A robust chat server handles disconnections gracefully. Let's verify your error handling works by simulating real-world scenarios like network drops and server shutdowns.

  • Switch to the Bob client terminal.
  • Press Ctrl+C to force-disconnect Bob from the chat.
  • Switch to the Alice client terminal. You should see a leave announcement:
[Server] Bob has left the chat.
  • Switch to the server terminal. You should see a log entry confirming Bob was removed from the clients dictionary.

Why does this work?

When Bob presses Ctrl+C, the client socket closes. On the server side, conn.recv(1024) returns empty bytes, which breaks the message loop. The finally block then removes Bob from the dictionary and broadcasts the leave message.

Now test reconnecting after a disconnect.

  • In the terminal where Bob was disconnected, run the client again:
python3 client.py
  • Enter Bob as the username again and press Enter.
  • Verify that Alice sees the join announcement again, confirming Bob reconnected successfully.

Finally, test what happens when the server shuts down.

  • Switch to the server terminal.
  • Press Ctrl+C to stop the server.
  • Switch to each client terminal. You should see:
[Disconnected from server]

What triggers the disconnection message?

When the server closes, all client sockets break. Each client's receive_messages thread detects this because sock.recv(1024) returns empty bytes. The thread then prints the disconnection message and exits.

Client not showing the disconnection message?

If the client appears frozen after the server stops, press Enter in the client terminal. The input() call blocks the main thread, so the disconnection message from the background thread might not appear until the main thread cycles.

help me understand why my chat client doesn't immediately show disconnection when the server stops

Secret mission

Add Private Messaging

Your chat room broadcasts every message to everyone. What if users want to have a private conversation? In this secret mission, you'll add a /whisper command that sends messages directly to a specific user, invisible to everyone else.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using Python's standard library, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files, or delete them entirely.

Resources you used:

  • The chat-room project folder on your Desktop (contains server.py and client.py).
  • Any running server or client processes in your Terminal windows.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your chat room, adding features, or showing it to friends.

  • Your chat-room folder stays on your Desktop, ready to use anytime.
  • To start the chat room again later, open Terminal, navigate to the folder, and run python3 server.py.

Pause - I'll come back to this later

Stop any running processes to free up your Terminal windows. Your project files stay on disk for next time.

  • In each Terminal window running the server or a client, press Ctrl+C to stop the process.
  • Close the extra Terminal tabs or windows you opened for testing.
  • Your chat-room folder remains on your Desktop. You can restart anytime with python3 server.py.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your chat server and client code permanently.

  • Stop any running server or client processes by pressing Ctrl+C in each Terminal window.
  • Delete the project folder by running this command in Terminal:
rm -rf ~/Desktop/chat-room
  • On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\chat-room
  • Confirm the folder is gone by running ls ~/Desktop (macOS) or checking your Desktop. You should no longer see chat-room listed.

Nice Work!

Nice Work!

You just built a real-time multi-client chat room from the ground up using Python's socket programming. Multiple users can now connect to your server simultaneously and exchange messages in real-time.

You've learned how to:

  • Build a TCP server that listens for connections, manages multiple clients with thread-safe locks, and broadcasts messages to all connected users.
  • Create a chat client that sends and receives messages concurrently using daemon threads for background I/O.
  • Implement the full socket lifecycle (socket(), bind(), listen(), accept(), recv()/send(), close()) with newline-delimited message framing for reliable communication over TCP streams.
  • Added private messaging with /whisper and /users command parsing for user-to-user communication.

Ready to quiz yourself?