DEV Community

giuseppe costanzi
giuseppe costanzi

Posted on

Design Patterns in a Real-World Tkinter Application: From Lateral Coupling to Clean Architecture

What you can build with Python's standard library


Beyond "Hello World"

Python, Tkinter, SQLite. The "batteries included" stack.

Most tutorials stop at a contact list or a todo app. But this stack can do much more — production applications solving real problems, with clean architecture and proper design patterns.

This article shows how. No external GUI frameworks, no ORMs, no heavyweight dependencies. Just the standard library, good patterns, and real-world requirements.

I'll walk you through Inventarium, a laboratory inventory management system running in production at a hospital. Along the way, we'll cover:

  • Singleton (two different implementations!)
  • Observer (decoupled view communication)
  • Registry (Toplevel window management)
  • Template Method
  • Mixin Architecture

All with real code, real problems, and real solutions.


The Problem: Lateral Coupling

Picture this: You have a Warehouse window showing stock levels. You have a Delivery window where you record incoming goods. When a delivery is saved, the warehouse view needs to refresh.

The naive solution:

# In delivery.py
def on_save(self):
    self.save_to_database()

    # Lateral coupling: Delivery KNOWS about Warehouse
    if "warehouse" in self.engine.dict_instances:
        warehouse = self.engine.dict_instances["warehouse"]
        warehouse.refresh_batches()
Enter fullscreen mode Exit fullscreen mode

This works. But now add:

  • Expiring view (shows batches about to expire)
  • Stocks view (shows stock levels)
  • Requests view (shows pending orders)

Suddenly, Delivery needs to know about all of them:

# This is getting ugly...
def on_save(self):
    self.save_to_database()

    if "warehouse" in self.engine.dict_instances:
        self.engine.dict_instances["warehouse"].refresh_batches()
    if "expiring" in self.engine.dict_instances:
        self.engine.dict_instances["expiring"].refresh()
    if "stocks" in self.engine.dict_instances:
        self.engine.dict_instances["stocks"].refresh()
    # And on and on...
Enter fullscreen mode Exit fullscreen mode

This is lateral coupling — modules that should be independent are now tangled together. Adding a new view means modifying every module that might affect it.


Solution: The Observer Pattern

The fix is elegant: instead of "push" (delivery tells everyone), use "pull" (interested parties subscribe).

The Subject (Engine)

class Engine(DBMS, Controller, Tools, Launcher, metaclass=_EngineMeta):
    """Main orchestrator with event system."""

    def __init__(self, database: str, autocommit: bool = True):
        super().__init__(database=database, autocommit=autocommit)

        # Event system: event_name -> [callbacks]
        self._subscribers = {}

    def subscribe(self, event: str, callback) -> None:
        """Register a callback for an event."""
        if event not in self._subscribers:
            self._subscribers[event] = []
        if callback not in self._subscribers[event]:
            self._subscribers[event].append(callback)

    def unsubscribe(self, event: str, callback) -> None:
        """Remove a callback from an event."""
        if event in self._subscribers:
            try:
                self._subscribers[event].remove(callback)
            except ValueError:
                pass

    def notify(self, event: str, data=None) -> None:
        """Notify all subscribers of an event."""
        for callback in self._subscribers.get(event, []):
            try:
                callback(data)
            except Exception:
                pass  # Subscriber might be dead, ignore
Enter fullscreen mode Exit fullscreen mode

The Publisher (Delivery)

# In delivery.py - clean and simple
def on_save(self):
    self.save_to_database()

    # Delivery doesn't know WHO listens, just WHAT happened
    self.engine.notify("stock_changed")
Enter fullscreen mode Exit fullscreen mode

The Subscribers (Warehouse, Expiring, etc.)

# In warehouse.py
class UI(ParentView):
    def __init__(self, parent):
        super().__init__(parent, name="warehouse")
        if self._reusing:
            return

        # Subscribe to events we care about
        self.engine.subscribe("stock_changed", self.on_stock_changed)
        self.engine.subscribe("batch_cancelled", self.on_stock_changed)

        self.init_ui()
        self.show()

    def on_stock_changed(self, data=None):
        """React to stock changes from any source."""
        self.refresh_batches()

    def on_cancel(self, evt=None):
        # Clean up subscriptions before closing
        self.engine.unsubscribe("stock_changed", self.on_stock_changed)
        self.engine.unsubscribe("batch_cancelled", self.on_stock_changed)
        super().on_cancel()
Enter fullscreen mode Exit fullscreen mode

The Events

Inventarium uses these events:

Event Fired by Subscribers
stock_changed Delivery Warehouse, Stocks
batch_cancelled Expiring Warehouse
category_changed Category Warehouse, Products
package_changed Package Warehouse
request_changed Requests Delivery

Now adding a new view is trivial — just subscribe to the events you care about. No existing code needs to change.


Singleton: Three Ways

Tkinter has a classic problem: users double-click menu items and create duplicate windows. Here are three approaches I use:

1. Singleton via __new__ (ParentView)

For main windows (Warehouse, Products, Suppliers), I use __new__ to intercept instance creation:

class ParentView(tk.Toplevel):
    """Base class for main views with singleton behavior."""

    _instance = None

    def __new__(cls, parent, *args, **kwargs):
        # If instance exists and is alive, reuse it
        if cls._instance is not None:
            try:
                if cls._instance.winfo_exists():
                    cls._instance.lift()
                    cls._instance.focus_set()
                    return cls._instance
            except Exception:
                pass

        # Create new instance
        obj = super().__new__(cls)
        cls._instance = obj
        return obj

    def __init__(self, parent, name=None):
        # Guard against re-initialization
        self._reusing = getattr(self, "_is_init", False)
        if self._reusing:
            return

        super().__init__(name=name)
        self._is_init = True

        # Anti-flash: hide during construction
        self.attributes("-alpha", 0.0)

        self.parent = parent
        self.engine = self.nametowidget(".").engine

    def show(self):
        """Show window after construction."""
        self.deiconify()
        self.attributes("-alpha", 1.0)

    def on_cancel(self, evt=None):
        """Clean up singleton reference."""
        type(self)._instance = None
        self._is_init = False
        self.destroy()
Enter fullscreen mode Exit fullscreen mode

Usage is clean:

class UI(ParentView):
    def __init__(self, parent):
        super().__init__(parent, name="warehouse")

        if self._reusing:
            return  # Window already exists, nothing to do

        # ... setup UI ...
        self.show()
Enter fullscreen mode Exit fullscreen mode

2. Registry Pattern (ChildView)

For dialogs (edit forms, detail views), I use a registry to prevent duplicate windows — we want one instance per dialog type, and if the user tries to open another, we close the existing one first:

class ChildView(tk.Toplevel):
    """Base class for CRUD dialogs with registry."""

    def __init__(self, parent, name=None):
        super().__init__(name=name)

        self.parent = parent
        self.engine = self.nametowidget(".").engine
        self._dialog_name = name

        # Register in global registry to track open windows
        if name:
            self.engine.dict_instances[name] = self

    def on_cancel(self, evt=None):
        # Unregister on close
        if self._dialog_name:
            self.engine.dict_instances.pop(self._dialog_name, None)
        self.destroy()
Enter fullscreen mode Exit fullscreen mode

The caller checks the registry before opening:

def on_edit(self):
    # Close existing dialog if open
    self.engine.close_instance("product")

    # Open new one
    obj = product.UI(self, index=product_id)
    obj.on_open(selected)
Enter fullscreen mode Exit fullscreen mode

3. Metaclass Singleton (Engine)

For the Engine class (the application's brain), I use a metaclass — the "proper" Python way:

class _EngineMeta(type):
    """Metaclass ensuring single Engine instance."""

    _instance = None

    def __call__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance


class Engine(DBMS, Controller, Tools, Launcher, metaclass=_EngineMeta):
    """Main orchestrator - singleton via metaclass."""
    pass
Enter fullscreen mode Exit fullscreen mode

This intercepts creation before __new__ and __init__ — the cleanest possible Singleton.

Note: The metaclass is "educational" in Inventarium since Engine is only instantiated once in App. But it demonstrates the pattern and protects against accidents.


Template Method: ParentView and ChildView

Both base classes implement the Template Method pattern — they define the skeleton, subclasses fill in the details:

┌─────────────────────────────────────────┐
│              ParentView                 │
├─────────────────────────────────────────┤
│ __new__()      → Singleton logic        │
│ __init__()     → Anti-flash setup       │
│ show()         → Display window         │
│ on_cancel()    → Cleanup + destroy      │
├─────────────────────────────────────────┤
│ Subclass provides:                      │
│ • init_ui()    → Build the interface    │
│ • Event handlers                        │
└─────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Every window follows the same lifecycle:

class UI(ParentView):
    def __init__(self, parent):
        super().__init__(parent, name="myview")
        if self._reusing:
            return

        self.protocol("WM_DELETE_WINDOW", self.on_cancel)
        self.init_ui()           # Subclass builds UI
        self.engine.center_window(self)
        self.show()              # Template method

    def init_ui(self):
        # Subclass-specific UI code
        pass

    def on_cancel(self, evt=None):
        # Subclass cleanup...
        super().on_cancel()      # Template method
Enter fullscreen mode Exit fullscreen mode

Mixin Architecture: The Engine

Engine combines four specialized mixins via multiple inheritance:

class Engine(DBMS, Controller, Tools, Launcher, metaclass=_EngineMeta):
    """
    Mixin Architecture (in MRO order):
        1. DBMS: Database connection and queries
        2. Controller: SQL builders and domain logic
        3. Tools: Shared utilities
        4. Launcher: Cross-platform file opener
    """
    pass
Enter fullscreen mode Exit fullscreen mode

Each mixin has a single responsibility:

Mixin Responsibility
DBMS read(), write(), execute()
Controller get_stock(), build_sql(), domain queries
Tools center_window(), set_icon(), validation
Launcher launch() — open files with default app

Views access everything through one interface:

# In any view
self.engine.read(True, sql, params)      # From DBMS
self.engine.get_stock()                   # From Controller
self.engine.center_window(self)           # From Tools
self.engine.launch(filepath)              # From Launcher
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Observer eliminates lateral coupling — publishers don't know subscribers, subscribers don't know publishers. Just events.

  2. Singleton has multiple valid implementations__new__ for simplicity, metaclass for purity, registry for context-sensitive uniqueness.

  3. Template Method creates consistency — every window follows the same lifecycle, reducing cognitive load.

  4. Mixins compose behavior — instead of deep inheritance hierarchies, combine focused capabilities.

  5. Real code teaches better than tutorials — patterns emerge from solving actual problems, not from contrived examples.


Try It Yourself

The full source is on GitHub: github.com/1966bc/inventarium

It's a working application managing real laboratory inventory — proof that Python's "batteries included" philosophy extends beyond tutorials to real-world, maintainable software.

Browse the code, steal the patterns, file issues if you find bugs.

Python + Tkinter + SQLite + Design Patterns = Production-ready applications.


Giuseppe Costanzi (@1966bc) is a biomedical laboratory technician at Sant'Andrea University Hospital, Mass Spectrometry Lab, in Rome. Inventarium is his solution for managing IVD product inventory.

This project was developed in collaboration with Claude (Anthropic). The AI pair-programming approach allowed focusing on architecture and functionality rather than getting bogged down in boilerplate code and syntax details — a workflow that proved transformative for development productivity.

Top comments (0)