Decoupling Complex Systems with Event-Driven Python Programming
We often think about events as ordered points in time that happen one after another, often with some kind of cause-effect relationship. But, in programming, events are often understood a bit differently. They are not necessarily “things that happen.” Events in programming are more often understood as independent units of information that can be processed by the program. And that very notion of events is a real cornerstone of concurrency within Python programming.
Concurrent programming is a Python programming paradigm for processing concurrent events. And there is a generalization of that paradigm that deals with the bare concept of events — no matter whether they are concurrent or not. This approach to programming, which treats programs as a flow of events, is called event-driven programming.
This article is an excerpt from the book, Expert Python Programming, Fourth Edition by Michal Jaworski and Tarek Ziade — A book that expresses many years of professional experience in building all kinds of applications with Python, from small system scripts done in a couple of hours to very large applications written by dozens of developers over several years.
What exactly is event-driven programming in Python Programming?
Event-driven programming focuses on the events (messages) and their flow between different software components. In fact, it can be found in many types of software. Historically, event-based Python programming is the most common paradigm for software that deals with direct human interaction. It means that it is a natural paradigm for graphical user interfaces. Everywhere the program needs to wait for some human input, that input can be modeled as events or messages. In such framing, an event-driven program is often just a collection of event/message handlers that respond to human interaction.
Events of course don’t have to be a direct result of user interaction. The architecture of any web application is also event-driven. Web browsers send requests to web servers on behalf of the user, and these requests are often processed as separate interaction events. Some of the requests will indeed be the result of direct user input (for example, submitting a form or clicking on a link), but don’t always have to be. Many modern applications can asynchronously synchronize information with a web server without any interaction from the user, and that communication happens silently without the user’s notice.
In summary, event-driven Python programming is a general way of coupling software components of various sizes and happens on various levels of software architecture. Depending on the scale and type of software architecture we’re dealing with, it can take various forms:
- It can be a concurrency model directly supported by a semantic feature of a given programming language (for example, async/await in Python)
- It can be a way of structuring application code with event dispatchers/handlers, signals, and so on
- It can be a general inter-process or inter-service communication architecture that allows for the coupling of independent software components in a larger system
Let’s discuss how event-driven programming is different from asynchronous programming in the next section.
Event-driven != asynchronous
Although event-driven programming is a paradigm that is extremely common for asynchronous systems, it doesn’t mean that every event-driven application must be asynchronous. It also doesn’t mean that event-driven Python programming is suited only for concurrent and asynchronous applications. Actually, the event-driven approach is extremely useful, even for decoupling problems that are strictly synchronous and definitely not concurrent.
Consider, for instance, database triggers that are available in almost every relational database system. A database trigger is a stored procedure that is executed in response to a certain event that happens in the database. This is a common building block of database systems that, among others, allows the database to maintain data consistency in scenarios that cannot be easily modeled with the mechanism of database constraints. For instance, the PostgreSQL database distinguishes three types of row-level events that can occur in either a table or a view:
- INSERT: emitted when new row is inserted
- UPDATE: emitted when existing row is updated
- DELETE: emitted when existing row is deleted
In the case of table rows, triggers can be defined to be executed either BEFORE or AFTER a specific event. So, from the perspective of event-procedure coupling, we can treat each AFTER/BEFORE trigger as a separate event. To better understand this, let’s consider the following example of database triggers in PostgreSQL:
CREATE TRIGGER before_user_update
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE PROCEDURE check_user(); CREATE TRIGGER after_user_update
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE PROCEDURE log_user_update();
In the preceding example, we have two triggers that are executed when a row in the users table is updated. The first one is executed before a real update occurs and the second one is executed after the update is done. This means that BEFORE UPDATE and AFTER UPDATE events are casually dependent and cannot be handled concurrently. On the other hand, similar sets of events occurring on different rows from different sessions can still be concurrent although that will depend on multiple factors (transaction or not, isolation level, scope of the trigger, and so on). This is a valid example of a situation where data modification in a database system can be modeled with event-based processing although the system as a whole isn’t fully asynchronous.
In the next section, we’ll take a look at event-driven programming in GUIs.
Event-driven programming in GUIs
Graphical User Interfaces (GUIs) are what many people think of when they hear the term event-driven programming. Event-driven programming is an elegant way of coupling user input to code in graphical user interfaces because it naturally captures the way people interact with graphical interfaces. Such interfaces often present the user with a plethora of components to interact with, and that interaction is almost always nonlinear. In complex interfaces, this interaction is often modeled through a collection of events that can be emitted by the user from different interface components.
The concept of events is common to most user interface libraries and frameworks, but different libraries use different design patterns to achieve the event-driven communication. Some libraries even use other notions to describe their architecture (for example, signals in Qt library). Still, the general pattern is almost always the same — every interface component (often called widget) can emit events upon interaction. Other components receive those events either by subscription or by directly attaching themselves to emitters as their event handlers. Depending on the GUI library, events can just be plain named signals stating that something has happened (for example, “widget A was clicked”), or be more complex messages containing additional information about the nature of the interaction. Such messages, for instance, can contain that a specific key has been pressed or what was the position of the mouse when the event was emitted.
We will discuss the differences of actual design patterns later in the Various styles of event-driven programming section but first, let’s take a look at the example Python GUI application that can be created with the use of the built-in tkinter module:
import this
from tkinter import *
from tkinter import messagebox rot13 = str.maketrans(
"ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz",
"NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm"
) def main_window(root: Tk):
frame = Frame(root)
frame.pack() zen_button = Button(frame, text="Python Zen", command=show_zen)
zen_button.pack(side=LEFT) def show_zen():
messagebox.showinfo("Zen of Python", this.s.translate(rot13)) if __name__ == "__main__":
root = Tk()
main_window(root)
root.mainloop() The Tk library that powers the tkinter module is usually bundled with Python distributions. If it's somehow not available on your operating system you should be easily able to install it through your system package manager. For instance, on Debian-based Linux distributions, you can easily install it for Python as the python3-tk package using the following command: sudo apt-get install python3-tk
The preceding GUI application displays a single Python Zen button. When the button is clicked, the application will open a new window containing the Zen of Python text that was imported from this module.
this module is a Python easter-egg. After import, it prints on standard output the 19 aphorisms that are guiding principles to Python’s design.
Our script starts with imports and the definition of a simple string translation table. It is necessary because the text of the Zen of Python is encrypted using ROT13 letter substitution cipher. It is a simple encryption algorithm that shifts every letter in the alphabet by 13 positions.
The binding of events happens directly in the Button widget constructor:
Button(frame, text="Python Zen", command=show_zen)
The command keyword argument defines the event handler that will be executed when the user clicks the button. This isn’t the only way of defining event handlers in tkinter. In our example, we have provided the show_zen() function that will display the decoded text of Zen of Python in a separate message box.
Every tkinter widget offers also a bind() method that can be used to define handlers of very specific events like mouse press/release, hover, and so on.
Most of the GUI frameworks work in a similar manner — you rarely work with raw keyboard and mouse inputs, but instead, attach your commands/callbacks to higher-level events such as the following:
- Checkbox state change
- Button clicked
- Option selected
- Window closed
In the next section, we’ll take a look at event-driven communication.
Event-driven communication
Event-driven programming is a very common practice for building distributed network applications. With event-driven programming, it is easier to split complex systems into isolated components that have a limited set of responsibilities and because of that, it is especially popular in service-oriented and microservice architectures. In such architectures, the flow of events happens not between classes or functions living inside of a single computer process, but between many networked services. In large and distributed architectures, the flow of events between services is often coordinated using special communication protocols (for example, AMQP and ZeroMQ) often with the help of dedicated services acting as message brokers. We will discuss some of these solutions later in the Event-driven architectures section.
However, you don’t need to have a formalized way of coordinating events, nor a dedicated event-handling service to consider your networked code as an event-based application. Actually, if you take a more detailed look at a typical Python web application, you’ll notice that most Python web frameworks have many things in common with GUI applications. Let’s, for instance, consider a simple web application that was written using the Flask microframework:
import this from flask import Flask app = Flask(__name__) rot13 = str.maketrans(
"ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz",
"NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm"
) def simple_html(body):
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Book Example</title>
</head>
<body>
{body}
</body>
</html>
""" @app.route('/')
def hello():
return simple_html("<a href=/zen>Python Zen</a>") @app.route('/zen')
def zen():
return simple_html(
"<br>".join(this.s.translate(rot13).split("\n"))
) if __name__ == '__main__':
app.run()
We’ve discussed examples of writing and executing simple Flask applications in Chapter 2, Modern Python Development Environments
If you compare the preceding listing with the example of the tkinter application from the previous section, you’ll notice that, structurally, they are very similar. Specific routes (paths) of HTTP requests translate to dedicated handlers. If we consider our application to be event-driven, then the request path can be treated as a binding between a specific event type (for example, a link being clicked) and the action handler. Similar to events in GUI applications, HTTP requests can contain additional data about interaction context. This information is, of course, structured. HTTP protocol defines multiple request methods (for example, POST, GET, PUT, and DELETE) and a few ways to transfer additional data (query string, request body, and headers).
The user does not communicate with our application directly as if it would when using GUI, but instead uses a web browser as their interface. But is this difference really that big? In fact, many cross-platform user interface libraries (such as Tcl/Tk, Qt, and GTK+) are just proxies between the application and the user’s operating system windowing APIs. So, in both cases, we deal with communication and events flowing through multiple system layers. It is just that, in web applications, layers are more evident, and communication is always explicit.
Summary of Python programming
In this article, we saw that event-driven programming is an important paradigm because it allows you to easily decouple even large and complex systems. It helps in defining clear boundaries between independent components and improves isolation between them.
We also studied that depending on the use case, event-driven programming can be used in multiple types of applications. It can take also different forms. In further sections of the book, we also go through the three major styles of event-driven programming.
Read more data science articles on OpenDataScience.com, including tutorials and guides from beginner to advanced levels! Subscribe to our weekly newsletter here and receive the latest news every Thursday. You can also get data science training on-demand wherever you are with our Ai+ Training platform.