Using QSettings to Save and Restore User Preferences in PyQt6

Build a settings dialog that persists user choices between application sessions
Heads up! You've already completed this tutorial.

I want to know how to use QSettings by having a dialog window (with radio buttons and all that) to serve as a settings UI, and then have QSettings serve 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:

python
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:

python
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:

Over 15,000 developers have bought Create GUI Applications with Python & Qt!
Create GUI Applications with Python & Qt6
Get the book

Downloadable ebook (PDF, ePub) & Complete Source code

[[ discount.discount_pc ]]% OFF for the next [[ discount.duration ]] [[discount.description ]] with the code [[ discount.coupon_code ]]

Purchasing Power Parity

Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]

  1. Wrap a QSettings instance for reading and writing values.
  2. Provide methods to sync widget states to and from settings.
  3. 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.

python
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:

python
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.

python
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:

python
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.

python
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:

python
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:

python
DEFAULTS = {
    "player_name": "",
    "engine_black": True,
    "engine_white": False,
}

Then when reading a value, look up its default to get the type:

python
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.

Get the book

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Using QSettings to Save and Restore User Preferences in PyQt6 was written by Martin Fitzpatrick.

Martin Fitzpatrick is the creator of Python GUIs, and has been developing Python/Qt applications for the past 12+ years. He has written a number of popular Python books and provides Python software development & consulting for teams and startups.