If you're working with QWebEngineView in PyQt6 and trying to save a page's HTML content to a file, you might run into this error:
TypeError: toHtml(self, Callable[[str], None]): not enough arguments
This happens because QWebEnginePage.toHtml() doesn't work the way you might expect. It doesn't simply return a string. Instead, it's an asynchronous method that takes a callback function — a function that will be called later, once the HTML content is ready.
This is a common source of confusion, so let's walk through what's going wrong and how to fix it.
The problem
Here's the code that triggers the error:
def save_file(self):
filename, _ = QFileDialog.getSaveFileName(
self, "Save Page as", "",
"Hypertext Markup Language (*.htm *.html);;"
"All files (*.*)"
)
if filename:
html = self.browser.page().toHtml()
with open(filename, 'w') as file:
file.write(html)
The line html = self.browser.page().toHtml() treats toHtml() as if it returns the HTML string directly. In the C++ Qt API, toHtml() is asynchronous and accepts a callback. PyQt6 mirrors this behavior — you must pass a callable (a function) that receives the HTML string as its argument.
The fix
To solve this, pass a callback function to toHtml(). The callback will receive the HTML string once it's available, and you can then write it to a file inside that callback.
Here's the corrected save_file method:
def save_file(self):
filename, _ = QFileDialog.getSaveFileName(
self, "Save Page as", "",
"Hypertext Markup Language (*.htm *.html);;"
"All files (*.*)"
)
if filename:
self.browser.page().toHtml(
lambda html: self._save_html_to_file(html, filename)
)
def _save_html_to_file(self, html, filename):
with open(filename, 'w') as file:
file.write(html)
Let's walk through what's happening:
- When the user picks a filename, we call
toHtml()and pass it a lambda — a small inline function. - That lambda receives the
htmlstring (provided by Qt once it's ready) and passes it along with thefilenameto our helper method_save_html_to_file. _save_html_to_filedoes the actual file writing.
We use a lambda here because toHtml() will call its callback with a single argument (the HTML string), but we also need access to the filename that the user chose. The lambda captures filename from the surrounding scope and passes both values to our helper method.
A simpler version
If you prefer, you can skip the separate helper method and do everything inside the lambda itself:
def save_file(self):
filename, _ = QFileDialog.getSaveFileName(
self, "Save Page as", "",
"Hypertext Markup Language (*.htm *.html);;"
"All files (*.*)"
)
if filename:
self.browser.page().toHtml(
lambda html: open(filename, 'w').write(html)
)
This works, but the previous version with a separate method is cleaner and easier to extend — for example, if you later want to add error handling or show a confirmation message after saving.
Why is toHtml() asynchronous?
QWebEnginePage runs web content in a separate process for security and performance reasons. When you ask for the page's HTML, the request has to cross a process boundary. The result isn't available immediately, so Qt uses a callback pattern: you provide a function, and Qt calls it when the HTML is ready.
This is different from the older QWebView (based on WebKit), where toHtml() did return a string directly. If you're following tutorials or reading older code that was written for QWebView, this kind of mismatch can catch you off guard.
Complete working example
Here's a minimal but complete browser application with a working save function, so you can try it out directly:
import sys
from PyQt6.QtCore import QUrl
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QToolBar, QAction,
QFileDialog, QStatusBar
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Mini Browser")
self.browser = QWebEngineView()
self.browser.setUrl(QUrl("https://www.pythonguis.com"))
self.setCentralWidget(self.browser)
# Navigation toolbar
toolbar = QToolBar("Navigation")
self.addToolBar(toolbar)
save_action = QAction("Save Page", self)
save_action.triggered.connect(self.save_file)
toolbar.addAction(save_action)
self.setStatusBar(QStatusBar(self))
self.browser.loadFinished.connect(
lambda: self.statusBar().showMessage("Page loaded")
)
self.show()
def save_file(self):
filename, _ = QFileDialog.getSaveFileName(
self, "Save Page As", "",
"Hypertext Markup Language (*.htm *.html);;"
"All files (*.*)"
)
if filename:
self.browser.page().toHtml(
lambda html: self._save_html_to_file(html, filename)
)
def _save_html_to_file(self, html, filename):
with open(filename, 'w') as file:
file.write(html)
self.statusBar().showMessage(f"Saved to {filename}")
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
Run this, click Save Page, choose a filename, and the HTML will be written to disk. You'll see a confirmation in the status bar once the file has been saved.
The pattern of passing a callback to an asynchronous method is something you'll encounter in other parts of QWebEnginePage too — for example, toPlainText() works the same way. Once you're comfortable with the callback approach, these methods become straightforward to use.
Bring Your PyQt/PySide Application to Market
Stuck in development hell? I'll help you get your project focused, finished and released. Benefit from years of practical experience releasing software with Python.