After spending over a decade developing desktop applications, I will say that event handling is essential for creating interactive PyQt6 applications. The signal-slot mechanism is the backbone of PyQt6’s event system. In this tutorial, I will explain how to handle button click events using signals and slots in PyQt6 with examples and screenshots.
Handle Button Click Events Using Signals and Slots in PyQt6
Before getting into implementation, let’s understand what signals and slots are in PyQt6.
Signals are emitted by widgets when something happens – like a button being clicked, a slider being moved, or text being edited. Signals are essentially notifications that something has occurred.
Slots are methods that respond to signals. When a signal is emitted, the connected slot(s) will be executed.
This signal-slot mechanism is what allows your application to respond to user interactions in a clean, organized way.
Read How to Create QLineEdit Widget in PyQt6?
Method 1: Basic Signal-Slot Connection for Button Clicks
Let’s start with the most simple approach to handle button clicks:
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Basic Button Click Example")
self.setGeometry(100, 100, 400, 200)
# Create central widget and layout
central_widget = QWidget()
layout = QVBoxLayout(central_widget)
# Create a label to display results
self.result_label = QLabel("Click the button below")
layout.addWidget(self.result_label)
# Create a button
self.button = QPushButton("Click Me!")
layout.addWidget(self.button)
# Connect the button's clicked signal to our custom slot
self.button.clicked.connect(self.button_clicked)
self.setCentralWidget(central_widget)
# Define the slot method
def button_clicked(self):
self.result_label.setText("Hello from San Francisco! Button was clicked.")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())You can see the output in the screenshot below.

In this example, I’ve:
- Created a simple window with a label and a button
- Connected the buttons
clickedsignal to our custombutton_clickedmethod - Updated the label’s text when the button is clicked
The key line here is self.button.clicked.connect(self.button_clicked) which establishes the signal-slot connection.
Check out How to Create QPushButton Widget in PyQt6?
Method 2: Connect Multiple Buttons to a Single Slot
In real-world applications, you’ll often have multiple buttons that need to perform similar actions. Here’s how to connect multiple buttons to a single slot:
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton,
QLabel, QVBoxLayout, QWidget, QHBoxLayout)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Multiple Buttons Example")
self.setGeometry(100, 100, 500, 200)
# Create central widget and layouts
central_widget = QWidget()
main_layout = QVBoxLayout(central_widget)
# Create a label to display results
self.result_label = QLabel("Select a city by clicking a button:")
main_layout.addWidget(self.result_label)
# Create a horizontal layout for buttons
button_layout = QHBoxLayout()
# Create multiple buttons for different cities
cities = ["New York", "Los Angeles", "Chicago", "Houston", "Miami"]
self.buttons = []
for city in cities:
button = QPushButton(city)
# Connect each button to the same slot
button.clicked.connect(self.city_button_clicked)
button_layout.addWidget(button)
self.buttons.append(button)
main_layout.addLayout(button_layout)
self.setCentralWidget(central_widget)
# Define the slot to handle all button clicks
def city_button_clicked(self):
# sender() returns the button that was clicked
button = self.sender()
if button:
self.result_label.setText(f"You selected: {button.text()}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())You can see the output in the screenshot below.

Here, I’ve used the sender() method to identify which button was clicked. This approach is more maintainable than creating separate slot methods for each button, especially when you have many similar buttons.
Read How to Create QLabel Widget in PyQt6?
Method 3: Pass Additional Data with Lambda Functions
Sometimes you need to pass additional data to your slot function when a button is clicked. Lambda functions are perfect for this:
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton,
QLabel, QVBoxLayout, QWidget, QGridLayout)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Lambda Button Example")
self.setGeometry(100, 100, 500, 300)
# Create central widget and layouts
central_widget = QWidget()
main_layout = QVBoxLayout(central_widget)
# Create a label to display results
self.result_label = QLabel("Select a state and its population:")
self.result_label.setStyleSheet("font-size: 14px; font-weight: bold;")
main_layout.addWidget(self.result_label)
# Create a grid layout for buttons
button_layout = QGridLayout()
# State data with populations
state_data = [
("California", 39.5, "Sacramento"),
("Texas", 29.0, "Austin"),
("Florida", 21.5, "Tallahassee"),
("New York", 19.8, "Albany"),
("Pennsylvania", 12.8, "Harrisburg")
]
# Create buttons with state names
for i, (state, population, capital) in enumerate(state_data):
row, col = divmod(i, 3) # Arrange in a grid, 3 columns
button = QPushButton(state)
# Connect with lambda to pass additional data
button.clicked.connect(
lambda checked, s=state, p=population, c=capital:
self.state_button_clicked(s, p, c)
)
button_layout.addWidget(button, row, col)
main_layout.addLayout(button_layout)
self.setCentralWidget(central_widget)
# Define the slot with parameters
def state_button_clicked(self, state, population, capital):
self.result_label.setText(
f"{state} has a population of {population} million people. "
f"Its capital is {capital}."
)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())You can see the output in the screenshot below.

The lambda function approach allows you to pass specific data associated with each button to your slot function. Note the use of default parameters in the lambda function (s=state, p=population, c=capital) – this is crucial to capture the current values in the loop.
Check out Basic Widgets in PyQt6
Method 4: Create Custom Signals and Slots
For more complex applications, you might want to create custom signals that carry additional data:
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton,
QLabel, QVBoxLayout, QWidget, QLineEdit)
from PyQt6.QtCore import pyqtSignal, pyqtSlot
class OrderWidget(QWidget):
# Define a custom signal with order data
order_placed = pyqtSignal(str, str, float)
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
# Create input fields
self.product_input = QLineEdit()
self.product_input.setPlaceholderText("Product Name")
layout.addWidget(self.product_input)
self.quantity_input = QLineEdit()
self.quantity_input.setPlaceholderText("Quantity")
layout.addWidget(self.quantity_input)
# Create order button
self.order_button = QPushButton("Place Order")
self.order_button.clicked.connect(self.place_order)
layout.addWidget(self.order_button)
def place_order(self):
product = self.product_input.text()
quantity = self.quantity_input.text()
# Simple validation
if not product or not quantity:
return
try:
# Calculate a simple price (for demo purposes)
price = float(quantity) * 10.99
# Emit the custom signal with order data
self.order_placed.emit(product, quantity, price)
# Clear the inputs
self.product_input.clear()
self.quantity_input.clear()
except ValueError:
# Handle invalid quantity
pass
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Signals Example - Seattle Electronics")
self.setGeometry(100, 100, 500, 300)
# Create central widget and layout
central_widget = QWidget()
main_layout = QVBoxLayout(central_widget)
# Create order widget
self.order_widget = OrderWidget()
main_layout.addWidget(self.order_widget)
# Connect the custom signal to our slot
self.order_widget.order_placed.connect(self.process_order)
# Create a label to display results
self.result_label = QLabel("Enter product details and place an order")
main_layout.addWidget(self.result_label)
self.setCentralWidget(central_widget)
@pyqtSlot(str, str, float)
def process_order(self, product, quantity, price):
self.result_label.setText(
f"Order Received: {quantity} x {product}\n"
f"Total Price: ${price:.2f}"
)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())In this more advanced example, I’ve:
- Created a custom widget (
OrderWidget) with its signal (order_placed) - Defined the signal to carry specific data types using
pyqtSignal(str, str, float) - Connected this custom signal to a slot in the main window
- Used the
@pyqtSlotdecorator to specify the exact signature of our slot function
This approach is particularly valuable for complex applications where you need to pass structured data between different components.
Read How to Create Icons for Windows in PyQt6?
Method 5: Event Handling with Signal Connection Types
In PyQt6, when you connect a signal to a slot (event handler), it uses a specific connection type under the hood to manage how the signal is delivered—especially in multithreaded applications.
from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel
from PyQt6.QtCore import QObject, pyqtSignal, Qt, QThread, pyqtSlot, QTimer
class Worker(QObject):
signal = pyqtSignal()
def __init__(self):
super().__init__()
def run(self):
print("Worker thread started.")
self.signal.emit()
class ExampleWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Signal Connection Types")
self.setGeometry(100, 100, 400, 200)
layout = QVBoxLayout()
self.label = QLabel("Click the button to emit signal.")
layout.addWidget(self.label)
self.button = QPushButton("Emit Signal")
self.button.clicked.connect(self.emit_signal)
layout.addWidget(self.button)
self.setLayout(layout)
# Worker setup in a separate thread
self.thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.worker.signal.connect(self.on_signal_received, type=Qt.ConnectionType.QueuedConnection)
self.thread.started.connect(self.worker.run)
self.thread.start()
@pyqtSlot()
def on_signal_received(self):
print("Signal received in main thread.")
self.label.setText("Signal received!")
def emit_signal(self):
print("Button clicked. Starting worker thread...")
self.thread.start()
app = QApplication([])
window = ExampleWindow()
window.show()
app.exec()- We define a
Workerclass with a signal calledsignal. - The
Workerruns in a separate thread, and whenemit_signalis triggered, it starts that thread. - The signal is connected using
QueuedConnectionensuring the slot is executed in the main GUI thread. - We also avoid
DirectConnectionfor GUI updates because GUI components must only be updated in the main thread
Check out Types of Windows in PyQt6
Conclusion
In this tutorial, I explained how to handle button click events using signals and slots in PyQt6. I discussed five important methods such as basic signal-slot connection for button clicks, connecting multiple buttons to a single slot, passing additional data with lambda functions, creating custom signals and slots, and event handling with signal connection types.
You may like to read:
- How to Arrange Widgets Using QGridLayout in PyQt6?
- How to Use QVBoxLayout in PyQt6 for Vertical Layouts?
- How to Use QHBoxLayout in PyQt6 for Horizontal Layouts?

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.