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()
This works. But now add:
-
Expiringview (shows batches about to expire) -
Stocksview (shows stock levels) -
Requestsview (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...
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
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")
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()
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()
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()
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()
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)
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
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 │
└─────────────────────────────────────────┘
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
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
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
Key Takeaways
Observer eliminates lateral coupling — publishers don't know subscribers, subscribers don't know publishers. Just events.
Singleton has multiple valid implementations —
__new__for simplicity, metaclass for purity, registry for context-sensitive uniqueness.Template Method creates consistency — every window follows the same lifecycle, reducing cognitive load.
Mixins compose behavior — instead of deep inheritance hierarchies, combine focused capabilities.
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)