I want to know how to use
QSettingsby having a dialog window (with radio buttons and all that) to serve as a settings UI, and then haveQSettingsserve as a settings manager. I'm having a hard time putting those two things together to work in concert. How can I hook it all up so my application responds to the saved settings?
Most applications need a way to remember user preferences — things like which theme to use, what name the player entered, or which side of the board an engine plays. In Qt, QSettings provides a simple, cross-platform way to persist these kinds of values between application runs. It stores data as key-value pairs, much like a Python dictionary, but the values are saved to disk automatically (in the Windows registry, macOS plist files, or INI files on Linux).
The challenge many people run into is connecting QSettings to a settings dialog. QSettings is just a storage mechanism — it doesn't know anything about your widgets, and it won't update your UI for you. You need to write the glue code that reads values from your widgets and writes them into QSettings, and vice versa.
In this tutorial, we'll build a complete working example: a main window with a button that opens a settings dialog, a settings manager that handles reading and writing values, and a label that updates to reflect the current state of the settings.
Understanding QSettings
QSettings is a key-value store. You give it a key (a string) and a value, and it saves that value persistently. The next time your application starts, you can retrieve the value using the same key.
Here's the most basic usage:
from PyQt6.QtCore import QSettings
settings = QSettings("MyCompany", "MyApp")
# Save a value
settings.setValue("player_name", "Alice")
# Read a value back
name = settings.value("player_name")
print(name) # "Alice"
The two strings passed to QSettings — the organization name and application name — determine where the settings are stored on disk. On Windows, this creates entries in the registry. On macOS, it creates a plist file. On Linux, it creates an INI file. You don't need to worry about the details — Qt handles it all for you.
One thing to be aware of: QSettings stores everything as strings internally (at least on some platforms). This means that when you read a boolean value back, you might get the string "true" instead of the Python boolean True. To handle this, you can pass the type parameter when reading:
value = settings.value("engine_plays_black", type=bool)
This ensures you always get the correct Python type back.
Creating a Settings Manager
Rather than scattering QSettings calls throughout your application, it's a good idea to centralize settings access in a dedicated manager class. This keeps your code organized and gives you a single place to add features like change notifications.
Our settings manager will do three things:
Purchasing Power Parity
Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]- Wrap a
QSettingsinstance for reading and writing values. - Provide methods to sync widget states to and from settings.
- Emit a signal when settings have changed, so the rest of the application can respond.
The widget syncing works through a mapping dictionary. For each Qt widget type, we define which method reads the widget's current value (the "getter") and which method sets it (the "setter"), along with the expected Python type. This lets us handle different widget types generically.
from PyQt6.QtCore import QObject, QSettings, pyqtSignal
class SettingsManager(QObject):
"""Centralized manager for application settings."""
settings_changed = pyqtSignal()
# Maps widget class names to (getter, setter, type) tuples
widget_mappers = {
"QCheckBox": ("isChecked", "setChecked", bool),
"QLineEdit": ("text", "setText", str),
"QSpinBox": ("value", "setValue", int),
"QRadioButton": ("isChecked", "setChecked", bool),
}
def __init__(self, organization, application):
super().__init__()
self.settings = QSettings(organization, application)
def value(self, key, default=None, type=None):
"""Read a value from settings with optional type conversion."""
if type is not None:
return self.settings.value(key, defaultValue=default, type=type)
return self.settings.value(key, defaultValue=default)
def set_value(self, key, value):
"""Write a value to settings."""
self.settings.setValue(key, value)
def update_widgets_from_settings(self, widget_map):
"""Set widget states from stored settings values."""
for name, widget in widget_map.items():
cls_name = widget.__class__.__name__
mapper = self.widget_mappers.get(cls_name)
if mapper is None:
continue
getter, setter, dtype = mapper
value = self.settings.value(name, type=dtype)
if setter and value is not None:
fn = getattr(widget, setter)
fn(value)
def update_settings_from_widgets(self, widget_map):
"""Save widget states into settings."""
for name, widget in widget_map.items():
cls_name = widget.__class__.__name__
mapper = self.widget_mappers.get(cls_name)
if mapper is None:
continue
getter, setter, dtype = mapper
if getter:
fn = getattr(widget, getter)
value = fn()
if value is not None:
self.settings.setValue(name, value)
self.settings_changed.emit()
The widget_mappers dictionary uses class name strings as keys. This avoids needing to import every widget class just for the mapping. When we encounter a QRadioButton, we look up "QRadioButton" in the dictionary and learn that we should call isChecked() to read its state and setChecked() to set it, and that the value is a bool.
Create a single instance of this manager and import it wherever you need it in your application:
settings_manager = SettingsManager("MyCompany", "MyApp")
Building the Settings Dialog
The settings dialog is a standard QDialog with widgets that represent configurable options. When the dialog opens, it loads the current settings values into its widgets. When the user clicks OK, it saves the widget states back to settings. When the user clicks Cancel, nothing is saved — the changes are simply discarded.
This is where the connection between QSettings and your UI happens. The dialog defines a widget_map dictionary that maps setting key names to the corresponding widgets. This map is passed to the settings manager's sync methods.
from PyQt6.QtWidgets import (
QDialog,
QDialogButtonBox,
QGroupBox,
QLineEdit,
QRadioButton,
QVBoxLayout,
)
class SettingsDialog(QDialog):
"""A dialog for editing application settings."""
def __init__(self, settings_manager):
super().__init__()
self.settings_manager = settings_manager
self.setWindowTitle("Settings")
# Player settings
self.player_group = QGroupBox("Player")
self.player_name = QLineEdit()
player_layout = QVBoxLayout()
player_layout.addWidget(self.player_name)
self.player_group.setLayout(player_layout)
# Engine settings
self.engine_group = QGroupBox("Chess Engine")
self.engine_black = QRadioButton("Plays as Black")
self.engine_black.setChecked(True)
self.engine_white = QRadioButton("Plays as White")
engine_layout = QVBoxLayout()
engine_layout.addWidget(self.engine_black)
engine_layout.addWidget(self.engine_white)
self.engine_group.setLayout(engine_layout)
# Dialog buttons
buttons = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.button_box = QDialogButtonBox(buttons)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
# Main layout
layout = QVBoxLayout()
layout.addWidget(self.player_group)
layout.addWidget(self.engine_group)
layout.addWidget(self.button_box)
self.setLayout(layout)
# Map setting keys to widgets
self.widget_map = {
"player_name": self.player_name,
"engine_black": self.engine_black,
"engine_white": self.engine_white,
}
# Load current settings into widgets
self.load_settings()
# Save settings only when the dialog is accepted
self.accepted.connect(self.save_settings)
def load_settings(self):
"""Populate widgets with values from the settings store."""
self.settings_manager.update_widgets_from_settings(self.widget_map)
def save_settings(self):
"""Write widget values to the settings store."""
self.settings_manager.update_settings_from_widgets(self.widget_map)
Notice that save_settings is connected to self.accepted — not to the button box's accepted signal directly. The accepted signal on QDialog only fires when the dialog is actually accepted (i.e., the user clicked OK). If the user clicks Cancel, rejected fires instead, and save_settings never runs. The widget changes are simply lost when the dialog closes, which is exactly what we want.
A common mistake is to create the settings dialog once and reuse it. If you do that, the widgets will remember whatever state the user left them in — even if they clicked Cancel. The simplest approach is to create a fresh dialog instance each time you open it. The load_settings call in __init__ ensures the widgets always reflect the actual stored values.
Responding to Settings Changes
The settings manager emits a settings_changed signal whenever settings are saved. You can connect to this signal from anywhere in your application to update the UI or change behavior in response to new settings. If you're new to Qt's signal and slot mechanism, see our tutorial on signals, slots, and events in PyQt6.
In the main window, we connect this signal to a method that refreshes a label showing the current settings:
settings_manager.settings_changed.connect(self.update_display)
This pattern keeps things nicely decoupled. The main window doesn't need to know anything about the dialog — it just listens for the signal and reads the values it needs from the settings manager.
Complete Working Example
Here's the full application with all the pieces connected. You can copy this into a single file and run it. Try changing the settings, clicking OK, and watching the label update. Then try making changes and clicking Cancel — the label should remain unchanged.
import sys
from PyQt6.QtCore import QObject, QSettings, pyqtSignal
from PyQt6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QGroupBox,
QLabel,
QLineEdit,
QPushButton,
QRadioButton,
QVBoxLayout,
QWidget,
)
class SettingsManager(QObject):
"""Centralized manager for application settings."""
settings_changed = pyqtSignal()
widget_mappers = {
"QCheckBox": ("isChecked", "setChecked", bool),
"QLineEdit": ("text", "setText", str),
"QSpinBox": ("value", "setValue", int),
"QRadioButton": ("isChecked", "setChecked", bool),
}
def __init__(self, organization, application):
super().__init__()
self.settings = QSettings(organization, application)
def value(self, key, default=None, type=None):
"""Read a value from settings with optional type conversion."""
if type is not None:
return self.settings.value(key, defaultValue=default, type=type)
return self.settings.value(key, defaultValue=default)
def set_value(self, key, value):
"""Write a value to settings and notify listeners."""
self.settings.setValue(key, value)
self.settings_changed.emit()
def update_widgets_from_settings(self, widget_map):
"""Set widget states from stored settings values."""
for name, widget in widget_map.items():
cls_name = widget.__class__.__name__
mapper = self.widget_mappers.get(cls_name)
if mapper is None:
continue
getter, setter, dtype = mapper
value = self.settings.value(name, type=dtype)
if setter and value is not None:
fn = getattr(widget, setter)
fn(value)
def update_settings_from_widgets(self, widget_map):
"""Save widget states into settings and emit change signal."""
for name, widget in widget_map.items():
cls_name = widget.__class__.__name__
mapper = self.widget_mappers.get(cls_name)
if mapper is None:
continue
getter, setter, dtype = mapper
if getter:
fn = getattr(widget, getter)
value = fn()
if value is not None:
self.settings.setValue(name, value)
self.settings_changed.emit()
# Create a single shared instance
settings_manager = SettingsManager("SuperChess", "settings")
class SettingsDialog(QDialog):
"""A dialog for editing application settings."""
def __init__(self):
super().__init__()
self.setWindowTitle("Settings")
# Player settings
self.player_group = QGroupBox("Player")
self.player_name = QLineEdit()
self.player_name.setPlaceholderText("Enter player name")
player_layout = QVBoxLayout()
player_layout.addWidget(self.player_name)
self.player_group.setLayout(player_layout)
# Engine settings
self.engine_group = QGroupBox("Chess Engine")
self.engine_black = QRadioButton("Plays as Black")
self.engine_black.setChecked(True)
self.engine_white = QRadioButton("Plays as White")
engine_layout = QVBoxLayout()
engine_layout.addWidget(self.engine_black)
engine_layout.addWidget(self.engine_white)
self.engine_group.setLayout(engine_layout)
# Dialog buttons
buttons = (
QDialogButtonBox.StandardButton.Ok
| QDialogButtonBox.StandardButton.Cancel
)
self.button_box = QDialogButtonBox(buttons)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
# Main layout
layout = QVBoxLayout()
layout.addWidget(self.player_group)
layout.addWidget(self.engine_group)
layout.addWidget(self.button_box)
self.setLayout(layout)
# Map setting keys to widgets
self.widget_map = {
"player_name": self.player_name,
"engine_black": self.engine_black,
"engine_white": self.engine_white,
}
# Load current settings into widgets
self.load_settings()
# Save only when accepted
self.accepted.connect(self.save_settings)
def load_settings(self):
"""Populate widgets with current stored values."""
settings_manager.update_widgets_from_settings(self.widget_map)
def save_settings(self):
"""Write widget values to the settings store."""
settings_manager.update_settings_from_widgets(self.widget_map)
class MainWindow(QWidget):
"""Main application window."""
def __init__(self):
super().__init__()
self.setWindowTitle("SuperChess")
self.settings_button = QPushButton("Open Settings")
self.settings_button.pressed.connect(self.open_settings)
self.info_label = QLabel()
self.info_label.setWordWrap(True)
layout = QVBoxLayout()
layout.addWidget(self.settings_button)
layout.addWidget(self.info_label)
self.setLayout(layout)
# Update the display whenever settings change
settings_manager.settings_changed.connect(self.update_display)
self.update_display()
def open_settings(self):
"""Create and show the settings dialog."""
dialog = SettingsDialog()
dialog.exec()
def update_display(self):
"""Refresh the label to show current settings values."""
player = settings_manager.value("player_name", default="(not set)", type=str)
engine_black = settings_manager.value("engine_black", default=True, type=bool)
engine_white = settings_manager.value("engine_white", default=False, type=bool)
if engine_black:
engine_side = "Black"
elif engine_white:
engine_side = "White"
else:
engine_side = "Unknown"
self.info_label.setText(
f"Player: {player}\n"
f"Engine plays as: {engine_side}"
)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
When you run this, you'll see a window with a button and a label. Clicking the button opens the settings dialog. Any changes you make and confirm with OK are immediately reflected in the label, and they persist between application restarts. Clicking Cancel discards any changes you made in the dialog.
Handling the Type Problem
You may have noticed that we always pass type=bool or type=str when reading values back from QSettings. This is because, depending on the platform, QSettings may store all values as strings internally. Without the type parameter, reading a boolean value might return the string "true" instead of Python's True, which can cause TypeError exceptions when you try to pass that value to a method like setChecked().
The widget_mappers dictionary in our settings manager already includes the expected type for each widget class, so the update_widgets_from_settings method handles this automatically. When reading settings manually elsewhere in your code, remember to pass the type parameter:
is_black = settings_manager.value("engine_black", type=bool)
If you find this tedious, another approach is to keep a dictionary of default values on your settings manager. You can use the type of each default value to automatically determine the correct type when reading:
DEFAULTS = {
"player_name": "",
"engine_black": True,
"engine_white": False,
}
Then when reading a value, look up its default to get the type:
def value(self, key):
default = self.DEFAULTS.get(key)
if default is not None:
return self.settings.value(key, defaultValue=default, type=type(default))
return self.settings.value(key)
This removes the need to specify the type every time, and also gives you sensible defaults for settings that haven't been saved yet.
Summary
QSettings is a straightforward key-value store — you write values in and read them back out, and Qt takes care of persisting them to disk in a platform-appropriate way. It doesn't interact with your widgets or your application logic directly. The work of connecting your settings dialog to QSettings is yours to write, but with a small settings manager class and a widget mapping dictionary, you can keep that glue code clean and reusable.
The pattern we've used here — a settings manager with change notifications, a dialog that loads on open and saves on accept, and application code that listens for the change signal — scales well as your application grows. Add new widgets to the dialog, add their entries to the widget_map, and everything else works the same way. You can also use QSettings to save and restore your window's size and position between sessions, giving your users a polished experience. For more on the basic PyQt6 widgets available to use in your dialogs, see our widgets tutorial.
Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.