7.7. Observer

  • When the state of the object changes and you need to notify other objects about this change

  • Notify chart about changes in data to refresh

  • Spreadsheet formulas

  • Notification of social media channel subscribers

  • Push or pull style of communication

The Observer design pattern is a behavioral design pattern that allows an object (known as the subject) to notify other objects (known as observers) about changes in its state. The subject maintains a list of observers and provides methods to add and remove observers from this list. When the state of the subject changes, it sends a notification to all its observers. This pattern is particularly useful in event-driven programming.

7.7.1. Solution

class Channel:
    def __init__(self, name):
        self.name = name
        self.subscribers = []

    def subscribe(self, user):
        self.subscribers.append(user)

    def unsubscribe(self, user):
        self.subscribers.remove(user)

    def notify(self, video):
        for user in self.subscribers:
            user.update(f'New upload {video}')


class User:
    def __init__(self, username):
        self.username = username

    def update(self, message):
        print(f'{self.username} received notification: {message}')

Usage:

alice = User('alice')
bob = User('bob')
carol = User('carol')

channel = Channel('My Youtube Channel')
channel.subscribe(alice)
channel.subscribe(bob)
channel.subscribe(carol)

channel.notify('Video 1')
alice received notification: New upload Video 1
bob received notification: New upload Video 1
carol received notification: New upload Video 1

channel.unsubscribe(carol)

channel.notify('Video 2')
alice received notification: New upload Video 2
bob received notification: New upload Video 2
../../_images/designpatterns-observer.png

7.7.2. Use Case 1

class Chatroom:
    def __init__(self, name):
        self.name = name
        self.users = []

    def join(self, user):
        self.users.append(user)
        print(f"{user.name} joined {self.name}")

    def leave(self, user):
        self.users.remove(user)
        print(f"{user.name} left {self.name}")

    def send_message(self, message):
        print(f"[{self.name}] New message: {message}")
        for user in self.users:
            user.receive_message(message)


class Client:
    def __init__(self, name):
        self.name = name
        self.chatroom = None

    def join_chatroom(self, chatroom):
        self.chatroom = chatroom
        chatroom.join(self)

    def leave_chatroom(self):
        if self.chatroom:
            self.chatroom.leave(self)
            self.chatroom = None

    def send_message(self, message):
        if self.chatroom:
            self.chatroom.send_message(f"{self.name}: {message}")

    def receive_message(self, message):
        print(f"{self.name} received: {message}")


def main():
    # Create a chatroom (Subject)
    general = Chatroom("General")

    # Create users (Observers)
    alice = Client("Alice")
    bob = Client("Bob")
    carol = Client("Carol")

    # Users join the chatroom
    alice.join_chatroom(general)
    bob.join_chatroom(general)
    carol.join_chatroom(general)

    # Alice sends a message to the chatroom
    alice.send_message("Hello everyone!")

    # Bob leaves the chatroom
    bob.leave_chatroom()

    # Alice sends another message
    alice.send_message("Is anyone still here?")

main()
Alice joined General
Bob joined General
Carol joined General
[General] New message: Alice: Hello everyone!
Alice received: Alice: Hello everyone!
Bob received: Alice: Hello everyone!
Carol received: Alice: Hello everyone!
Bob left General
[General] New message: Alice: Is anyone still here?
Alice received: Alice: Is anyone still here?
Carol received: Alice: Is anyone still here?

7.7.3. Assignments

# %% About
# - Name: DesignPatterns Behavioral Observer
# - Difficulty: easy
# - Lines: 17
# - Minutes: 13

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% English
# 1. Create a chatroom application using classes Chatroom and user
# 2. Implement the Observer pattern
# 3. Run doctests - all must succeed

# %% Polish
# 1. Stwórz aplikację do czatowania używając klas Chatroom i user
# 2. Zaimplementuj wzorzec Obserwator
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isclass, ismethod

>>> assert isclass(Chatroom)
>>> assert isclass(User)
>>> assert hasattr(Chatroom, 'join')
>>> assert hasattr(Chatroom, 'leave')
>>> assert hasattr(Chatroom, 'broadcast')
>>> assert hasattr(User, 'receive')
>>> assert ismethod(Chatroom().join)
>>> assert ismethod(Chatroom().leave)
>>> assert ismethod(Chatroom().broadcast)
>>> assert ismethod(User('').receive)

>>> room = Chatroom()
>>> alice = User('Alice')
>>> bob = User('Bob')
>>> carol = User('Carol')

>>> room.join(alice)
>>> room.join(bob)
>>> room.join(carol)

>>> room.broadcast("Hello everyone!")
Alice received: Hello everyone!
Bob received: Hello everyone!
Carol received: Hello everyone!

>>> room.leave(bob)
>>> room.broadcast("Bob left the chat")
Alice received: Bob left the chat
Carol received: Bob left the chat
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% Imports

# %% Types
from typing import Callable, List
Chatroom: type
user: type
join: Callable[[object, object], None]
leave: Callable[[object, object], None]
broadcast: Callable[[object, str], None]
receive: Callable[[object, str], None]

# %% Data

# %% Result
class Chatroom:
    def __init__(self):
        self.users = []

    def join(self, users):
        pass

    def leave(self, users):
        pass

    def broadcast(self, message):
        pass


class User:
    def __init__(self, name):
        self.name = name

    def receive(self, message):
        pass