PySide6

Cross-platform GUI framework for Python based on Qt 6. Official Qt Company support with LGPL license. Easier for enterprise adoption than PyQt due to commercial usability. Features rich widgets and modern UI development.

DesktopCross-platformPythonGUIQt

Framework

PySide6

Overview

PySide6 is a cross-platform GUI framework for Python based on Qt 6. With official Qt Company support and LGPL license, it enables commercial use and is more enterprise-friendly than PyQt. Features rich widgets and modern UI development capabilities.

Details

PySide6 has established itself as the mainstream choice for Python desktop development in 2025. Qt 6 support provides latest UI features and performance improvements, with commercial project adoption rate exceeding PyQt. Demand is particularly increasing in finance and scientific fields.

With official support from The Qt Company and fewer commercial restrictions due to LGPL licensing, enterprise adoption is progressing. Comprehensive features for professional desktop application development are provided, including visual UI design with Qt Designer, QML integration, and rich widget libraries.

PySide6 is adopted in many prominent applications such as Maya, FreeCAD, Eric IDE, Anki, and Spyder, making it the standard choice for Python developers building professional desktop applications.

Pros and Cons

Pros

  • Official Support: Official support and continuous development by Qt Company
  • License Flexibility: LGPL license enables easier commercial use
  • Pythonic API: Pythonic interface that's easy to learn
  • Latest Qt 6 Features: Modern UI features and performance
  • Rich Widgets: All functionality needed for desktop apps
  • Qt Designer Integration: Visual UI design tools
  • QML Support: Declarative UI and advanced animations
  • Cross-platform: Windows, macOS, Linux support
  • Comprehensive Documentation: Detailed docs and samples

Cons

  • App Size: Large binary size due to Qt dependencies
  • Memory Usage: High memory consumption due to rich GUI
  • Distribution Complexity: Dependency management and packaging challenges
  • Learning Curve: Understanding Qt-specific concepts and signals/slots required
  • Package Size: Large download size during pip installation

Key Links

Code Examples

Hello World Application

# hello_world.py
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QPushButton
from PySide6.QtCore import Qt

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PySide6 Hello World")
        self.setGeometry(100, 100, 300, 200)
        
        # Central widget
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # Layout
        layout = QVBoxLayout(central_widget)
        
        # Label
        self.label = QLabel("Hello, PySide6!", self)
        self.label.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.label)
        
        # Button
        button = QPushButton("Click me", self)
        button.clicked.connect(self.on_button_clicked)
        layout.addWidget(button)
        
        self.click_count = 0
    
    def on_button_clicked(self):
        self.click_count += 1
        self.label.setText(f"Clicked {self.click_count} times")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

Signals and Slots

# signals_slots.py
import sys
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, 
                               QVBoxLayout, QHBoxLayout, QLabel, 
                               QPushButton, QSlider, QLineEdit)
from PySide6.QtCore import Qt, QTimer, Signal, QObject

class DataModel(QObject):
    # Custom signals
    value_changed = Signal(int)
    message_updated = Signal(str)
    
    def __init__(self):
        super().__init__()
        self._value = 0
        self._timer = QTimer()
        self._timer.timeout.connect(self.update_value)
    
    def start_auto_update(self):
        self._timer.start(1000)  # 1 second interval
    
    def stop_auto_update(self):
        self._timer.stop()
    
    def update_value(self):
        self._value += 1
        self.value_changed.emit(self._value)
        self.message_updated.emit(f"Auto updated: {self._value}")
    
    def set_manual_value(self, value):
        self._value = value
        self.value_changed.emit(self._value)
        self.message_updated.emit(f"Manually set: {self._value}")

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Signals and Slots Example")
        self.setGeometry(100, 100, 400, 300)
        
        # Data model
        self.model = DataModel()
        
        # UI setup
        self.setup_ui()
        
        # Connect signals
        self.model.value_changed.connect(self.on_value_changed)
        self.model.message_updated.connect(self.on_message_updated)
    
    def setup_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        
        # Value display label
        self.value_label = QLabel("Value: 0")
        layout.addWidget(self.value_label)
        
        # Message label
        self.message_label = QLabel("Message: None")
        layout.addWidget(self.message_label)
        
        # Slider
        slider_layout = QHBoxLayout()
        slider_layout.addWidget(QLabel("Manual Set:"))
        self.slider = QSlider(Qt.Horizontal)
        self.slider.setRange(0, 100)
        self.slider.valueChanged.connect(self.model.set_manual_value)
        slider_layout.addWidget(self.slider)
        layout.addLayout(slider_layout)
        
        # Buttons
        button_layout = QHBoxLayout()
        
        self.start_button = QPushButton("Start Auto Update")
        self.start_button.clicked.connect(self.model.start_auto_update)
        button_layout.addWidget(self.start_button)
        
        self.stop_button = QPushButton("Stop Auto Update")
        self.stop_button.clicked.connect(self.model.stop_auto_update)
        button_layout.addWidget(self.stop_button)
        
        layout.addLayout(button_layout)
    
    def on_value_changed(self, value):
        self.value_label.setText(f"Value: {value}")
        # Update slider (temporarily block signals to avoid loops)
        self.slider.blockSignals(True)
        self.slider.setValue(min(value, 100))
        self.slider.blockSignals(False)
    
    def on_message_updated(self, message):
        self.message_label.setText(f"Message: {message}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

Qt Designer Integration

# designer_ui.py
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtUiTools import QUiLoader
from PySide6.QtCore import QFile, QIODevice

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.load_ui()
        self.setup_connections()
    
    def load_ui(self):
        # Load .ui file
        ui_file = QFile("mainwindow.ui")
        if ui_file.open(QIODevice.ReadOnly):
            loader = QUiLoader()
            self.ui = loader.load(ui_file, self)
            ui_file.close()
            
            # Set UI as central widget
            self.setCentralWidget(self.ui)
        else:
            print("Failed to load UI file")
    
    def setup_connections(self):
        # Get references to UI elements and connect signals
        if hasattr(self.ui, 'pushButton'):
            self.ui.pushButton.clicked.connect(self.on_button_clicked)
        
        if hasattr(self.ui, 'lineEdit'):
            self.ui.lineEdit.textChanged.connect(self.on_text_changed)
    
    def on_button_clicked(self):
        if hasattr(self.ui, 'label'):
            self.ui.label.setText("Button clicked!")
    
    def on_text_changed(self, text):
        if hasattr(self.ui, 'label_2'):
            self.ui.label_2.setText(f"Input: {text}")

# Alternative: Use UI converted to .py file
# pyside6-uic mainwindow.ui -o ui_mainwindow.py
# from ui_mainwindow import Ui_MainWindow
# 
# class MainWindow(QMainWindow):
#     def __init__(self):
#         super().__init__()
#         self.ui = Ui_MainWindow()
#         self.ui.setupUi(self)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

QML Integration

# qml_integration.py
import sys
import os
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QmlElement, qmlRegisterType
from PySide6.QtQuick import QQuickView
from PySide6.QtCore import QObject, Signal, Slot, Property

# Python class for use in QML
QML_IMPORT_NAME = "DataModel"
QML_IMPORT_MAJOR_VERSION = 1

@QmlElement
class DataModel(QObject):
    # Signals
    dataChanged = Signal(str)
    
    def __init__(self):
        super().__init__()
        self._data = "Initial Data"
    
    # Properties
    def get_data(self):
        return self._data
    
    def set_data(self, data):
        if self._data != data:
            self._data = data
            self.dataChanged.emit(data)
    
    data = Property(str, get_data, set_data, notify=dataChanged)
    
    # Slots (callable from QML)
    @Slot()
    def update_data(self):
        import random
        new_data = f"Random Data: {random.randint(1, 100)}"
        self.set_data(new_data)
    
    @Slot(str, result=str)
    def process_input(self, input_text):
        return f"Processed: {input_text.upper()}"

def main():
    app = QGuiApplication(sys.argv)
    
    # Register Python class with QML engine
    qmlRegisterType(DataModel, "DataModel", 1, 0, "DataModel")
    
    # Load QML file
    view = QQuickView()
    view.setSource("main.qml")
    view.show()
    
    return app.exec()

if __name__ == "__main__":
    sys.exit(main())

Database Operations

# database_example.py
import sys
import sqlite3
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, 
                               QVBoxLayout, QHBoxLayout, QTableWidget, 
                               QTableWidgetItem, QPushButton, QLineEdit, 
                               QLabel, QMessageBox)
from PySide6.QtCore import Qt

class DatabaseApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PySide6 Database Operations")
        self.setGeometry(100, 100, 600, 400)
        
        # Initialize database
        self.init_database()
        
        # Create UI
        self.setup_ui()
        
        # Load data
        self.load_data()
    
    def init_database(self):
        self.conn = sqlite3.connect("users.db")
        cursor = self.conn.cursor()
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL,
                age INTEGER
            )
        """)
        self.conn.commit()
    
    def setup_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        
        # Input form
        form_layout = QHBoxLayout()
        
        form_layout.addWidget(QLabel("Name:"))
        self.name_input = QLineEdit()
        form_layout.addWidget(self.name_input)
        
        form_layout.addWidget(QLabel("Email:"))
        self.email_input = QLineEdit()
        form_layout.addWidget(self.email_input)
        
        form_layout.addWidget(QLabel("Age:"))
        self.age_input = QLineEdit()
        form_layout.addWidget(self.age_input)
        
        add_button = QPushButton("Add")
        add_button.clicked.connect(self.add_user)
        form_layout.addWidget(add_button)
        
        layout.addLayout(form_layout)
        
        # Table
        self.table = QTableWidget()
        self.table.setColumnCount(4)
        self.table.setHorizontalHeaderLabels(["ID", "Name", "Email", "Age"])
        layout.addWidget(self.table)
        
        # Buttons
        button_layout = QHBoxLayout()
        
        refresh_button = QPushButton("Refresh")
        refresh_button.clicked.connect(self.load_data)
        button_layout.addWidget(refresh_button)
        
        delete_button = QPushButton("Delete")
        delete_button.clicked.connect(self.delete_user)
        button_layout.addWidget(delete_button)
        
        layout.addLayout(button_layout)
    
    def add_user(self):
        name = self.name_input.text().strip()
        email = self.email_input.text().strip()
        age_text = self.age_input.text().strip()
        
        if not name or not email:
            QMessageBox.warning(self, "Warning", "Name and email are required")
            return
        
        try:
            age = int(age_text) if age_text else None
            cursor = self.conn.cursor()
            cursor.execute(
                "INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
                (name, email, age)
            )
            self.conn.commit()
            
            # Clear input fields
            self.name_input.clear()
            self.email_input.clear()
            self.age_input.clear()
            
            # Update table
            self.load_data()
            
        except ValueError:
            QMessageBox.warning(self, "Error", "Please enter a valid age")
        except sqlite3.IntegrityError:
            QMessageBox.warning(self, "Error", "This email already exists")
    
    def load_data(self):
        cursor = self.conn.cursor()
        cursor.execute("SELECT id, name, email, age FROM users")
        rows = cursor.fetchall()
        
        self.table.setRowCount(len(rows))
        for row_index, row_data in enumerate(rows):
            for col_index, col_data in enumerate(row_data):
                item = QTableWidgetItem(str(col_data if col_data is not None else ""))
                self.table.setItem(row_index, col_index, item)
    
    def delete_user(self):
        current_row = self.table.currentRow()
        if current_row < 0:
            QMessageBox.warning(self, "Warning", "Please select a row to delete")
            return
        
        user_id = self.table.item(current_row, 0).text()
        
        reply = QMessageBox.question(
            self, "Confirm", 
            f"Delete user ID {user_id}?",
            QMessageBox.Yes | QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            cursor = self.conn.cursor()
            cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
            self.conn.commit()
            self.load_data()
    
    def closeEvent(self, event):
        self.conn.close()
        event.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = DatabaseApp()
    window.show()
    sys.exit(app.exec())

Project Setup and Execution

# Create virtual environment (recommended)
python -m venv pyside6_env
source pyside6_env/bin/activate  # Linux/macOS
# or
pyside6_env\Scripts\activate  # Windows

# Install PySide6
pip install PySide6

# Use Qt Designer
designer  # Launch Qt Designer

# Convert .ui file to .py file
pyside6-uic mainwindow.ui -o ui_mainwindow.py

# Convert resource files (.qrc)
pyside6-rcc resources.qrc -o resources_rc.py

# Run application
python hello_world.py

# Create standalone executable (using PyInstaller)
pip install pyinstaller
pyinstaller --onefile --windowed hello_world.py

# Create requirements.txt
pip freeze > requirements.txt

Example Project Structure

my_pyside6_app/
├── main.py                 # Main application
├── ui/
│   ├── __init__.py
│   ├── mainwindow.ui       # Qt Designer file
│   └── ui_mainwindow.py    # Converted UI file
├── resources/
│   ├── icons/
│   ├── images/
│   └── resources.qrc       # Resource file
├── models/
│   ├── __init__.py
│   └── data_model.py       # Data models
├── views/
│   ├── __init__.py
│   └── custom_widgets.py   # Custom widgets
├── utils/
│   ├── __init__.py
│   └── database.py         # Database utilities
├── requirements.txt
└── README.md