09 Sep 2026

feedPlanet Python

Python GUIs: How to Check if a QLineEdit is Empty in Python — Empty strings are falsey in Python

A reader asked:

I just want to know, how do I check whether a QLineEdit is empty or not?

The QLineEdit class doesn't have an isEmpty() method which you can call to find out if the line edit is empty, but we don't need one! Instead we can get the current text using .text() and then check if the returned value is an empty string.

Checking QLineEdit Text with .text()

In the code below lineedit is our already created QLineEdit widget.

python
text = lineedit.text()
if text == '': # if the line edit is empty, .text() will return an empty string.
     # do something

Using Python's Falsey Empty Strings

We can simplify this further. In Python empty strings are falsey -- they are considered False values in conditional expressions. So instead of checking the string is empty, we can check if it is true (non-empty) or false (empty).

python
if lineedit.text():
     # do something if there is content in the line edit.

Or, to check if it is empty:

python
if not lineedit.text():
     # do something if the line edit is empty.

Complete Example: Detecting Empty QLineEdit with Signals

Below is a small demo application which updates a label to indicate if the QLineEdit has text in it or not. In this we use Qt signals to send the current text to a slot method every time it is updated.

python
import sys

from PyQt5.QtWidgets import QApplication, QLabel, QLineEdit, QVBoxLayout, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.lineedit = QLineEdit()
        self.lineedit.textChanged.connect(self.text_changed)

        self.label = QLabel()

        vlayout = QVBoxLayout()
        vlayout.addWidget(self.lineedit)
        vlayout.addWidget(self.label)

        self.setLayout(vlayout)

    def text_changed(self, s):

        # s contains the text of the line edit, we could also test self.lineedit.text()

        if s:
            self.label.setText("Not empty")

        else:
            self.label.setText("Empty")


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec_()
python
import sys

from PyQt6.QtWidgets import QApplication, QLabel, QLineEdit, QVBoxLayout, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.lineedit = QLineEdit()
        self.lineedit.textChanged.connect(self.text_changed)

        self.label = QLabel()

        vlayout = QVBoxLayout()
        vlayout.addWidget(self.lineedit)
        vlayout.addWidget(self.label)

        self.setLayout(vlayout)

    def text_changed(self, s):

        # s contains the text of the line edit

        if s:
            self.label.setText("Not empty")

        else:
            self.label.setText("Empty")


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec()

python
import sys

from PySide2.QtWidgets import QApplication, QLabel, QLineEdit, QVBoxLayout, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.lineedit = QLineEdit()
        self.lineedit.textChanged.connect(self.text_changed)

        self.label = QLabel()

        vlayout = QVBoxLayout()
        vlayout.addWidget(self.lineedit)
        vlayout.addWidget(self.label)

        self.setLayout(vlayout)

    def text_changed(self, s):

        # s contains the text of the line edit

        if s:
            self.label.setText("Not empty")

        else:
            self.label.setText("Empty")


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec_()


python
import sys

from PySide6.QtWidgets import QApplication, QLabel, QLineEdit, QVBoxLayout, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.lineedit = QLineEdit()
        self.lineedit.textChanged.connect(self.text_changed)

        self.label = QLabel()

        vlayout = QVBoxLayout()
        vlayout.addWidget(self.lineedit)
        vlayout.addWidget(self.label)

        self.setLayout(vlayout)

    def text_changed(self, s):

        # s contains the text of the line edit

        if s:
            self.label.setText("Not empty")

        else:
            self.label.setText("Empty")


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec_()


Run the above and you'll see the label update as you add and remove text in the QLineEdit.

Empty QLineEdit widget in PyQt/PySide

QLineEdit with content in PyQt/PySide

This approach works across all Python Qt bindings including PyQt5, PyQt6, PySide2 and PySide6. By leveraging Python's truthiness checks on strings, you can validate QLineEdit input cleanly without needing a dedicated isEmpty() method. For more advanced input validation techniques, you may also want to look at input validation in Tkinter or explore the full range of PyQt6 widgets available for building your applications.

For an in-depth guide to building Python GUIs with PySide6 see my book, Create GUI Applications with Python & Qt6.

09 Sep 2026 6:00am GMT

Python GUIs: Fixing Crashes When Using NumPy Arrays with QImage in Qt Threads — How to safely pass image data between threads when streaming video or updating displays

I'm using a threaded runner to stream a live video feed by converting a NumPy array to a QImage, then to a QPixmap, and displaying it on a QLabel. But I'm frequently encountering crashes when the label is resized too quickly or the scroll area is scrolled. Could this be a problem with the QImage memory buffer getting cleared before the QPixmap can update? Is this fixable, or is it a fundamental issue with threads in Python/Qt?

This is a common problem when working with NumPy arrays and QImage across threads. The good news is that it's fixable. The crashes come from how QImage handles the underlying memory of a NumPy array.

Why the crash happens

When you create a QImage from a NumPy array, QImage doesn't copy the data. Instead it holds a reference to the original memory buffer provided by the NumPy array.

python
import numpy as np
from PyQt6.QtGui import QImage

# Create a NumPy array (e.g. a video frame)
array = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)

# QImage points to the same memory - no copy is made
image = QImage(
    array.data,
    array.shape[1],
    array.shape[0],
    array.strides[0],
    QImage.Format.Format_RGB8888,
)

This is efficient, but it creates a dangerous situation in a multithreaded application. If the NumPy array is modified or goes out of scope in the worker thread while the GUI thread is still using the QImage to paint, the memory that QImage is pointing to may no longer be valid. The result is a segfault or (worse) a silent crash without any information about what has happened.

This is especially likely when frames are arriving quickly (as with a live video feed) and the GUI is being redrawn frequently - for example, during a resize or a scroll.

The fix: copy the image data

The simplest and most reliable fix is to make sure the QImage owns its own copy of the pixel data before you pass it to the GUI thread. You can do this by calling .copy() on the QImage:

python
image = QImage(
    array.data,
    array.shape[1],
    array.shape[0],
    array.strides[0],
    QImage.Format.Format_RGB888,
).copy()

The .copy() call creates a new QImage with its own independent memory buffer. Now it doesn't matter if the original NumPy array changes or disappears - the QImage is safe to use from the GUI thread.

If you've already tried using .copy() and it hasn't worked, bear in mind that where you use the copy matters just as much as using it at all.

Where to copy matters

If you create the QImage in the worker thread and emit it via a signal, the copy needs to happen before the signal is emitted. If the signal carries a reference to the original (non-copied) QImage, the data might still be invalidated before the GUI thread processes it.

Here's the pattern that works:

python
# In the worker thread
image = QImage(
    frame.data,
    frame.shape[1],
    frame.shape[0],
    frame.strides[0],
    QImage.Format.Format_RGB888,
).copy()  # Copy immediately, before emitting

self.signals.result.emit(image)  # Now safe to send to GUI thread

And in the main thread, connect that signal to a slot that updates the display:

python
def update_display(self, image):
    pixmap = QPixmap.fromImage(image)
    self.label.setPixmap(pixmap)

Because the QImage was copied before it crossed the thread boundary, the GUI thread has full ownership of the data and can paint it safely.

Use signals to control the update flow

Another source of crashes is calling GUI methods directly from a worker thread. In Qt, all GUI updates must happen on the main thread. If you're calling label.setPixmap(...) from inside a worker or a callback running on a background thread, that's undefined behavior and will eventually crash.

The solution is to always use signals and slots to communicate between threads. Emit a signal from the worker carrying the processed image, and connect it to a slot on the main thread that performs the GUI update.

This also gives you a natural way to throttle updates. If frames are arriving faster than the GUI can paint them, you can use a flag to skip frames that arrive while the previous one is still being displayed.

Complete working example

Here's a full example that simulates a video feed using a QRunnable and a QThreadPool. It generates random NumPy frames in a background thread and safely displays them on a QLabel. If you're new to running background tasks with QThreadPool, see our detailed guide to multithreading PyQt6 applications.

python
import sys
import time

import numpy as np
from PyQt6.QtCore import (
    QObject,
    QRunnable,
    QThreadPool,
    pyqtSignal,
    pyqtSlot,
)
from PyQt6.QtGui import QImage, QPixmap
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QScrollArea,
    QVBoxLayout,
    QWidget,
)


class WorkerSignals(QObject):
    frame_ready = pyqtSignal(QImage)
    finished = pyqtSignal()


class VideoWorker(QRunnable):
    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.running = True

    @pyqtSlot()
    def run(self):
        while self.running:
            # Simulate a video frame (e.g. from a camera or stream)
            frame = np.random.randint(
                0, 255, (480, 640, 3), dtype=np.uint8
            )

            # Create QImage and copy it immediately so it owns its data
            image = QImage(
                frame.data,
                frame.shape[1],
                frame.shape[0],
                frame.strides[0],
                QImage.Format.Format_RGB888,
            ).copy()

            # Emit the safe, copied image to the main thread
            self.signals.frame_ready.emit(image)

            # Simulate ~30 fps
            time.sleep(1 / 30)

        self.signals.finished.emit()

    def stop(self):
        self.running = False


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Threaded Video Display")

        self.label = QLabel("Waiting for frames...")
        self.label.setScaledContents(True)

        scroll_area = QScrollArea()
        scroll_area.setWidget(self.label)
        scroll_area.setWidgetResiizable(True)

        container = QWidget()
        layout = QVBoxLayout(container)
        layout.addWidget(scroll_area)
        self.setCentralWidget(container)

        self.resize(700, 520)

        # Set up threading
        self.threadpool = QThreadPool()
        self.worker = VideoWorker()
        self.worker.signals.frame_ready.connect(self.update_display)
        self.worker.signals.finished.connect(self.on_finished)
        self.threadpool.start(self.worker)

    def update_display(self, image):
        """Runs on the main thread - safe to update the GUI here."""
        pixmap = QPixmap.fromImage(image)
        self.label.setPixmap(pixmap)

    def on_finished(self):
        print("Worker finished.")

    def closeEvent(self, event):
        self.worker.stop()
        self.threadpool.waitForDone(2000)
        event.accept()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

When you run this, you'll see a window displaying rapidly changing random noise - a stand-in for a real video stream. You can resize the window and scroll around without crashes, because the image data is safely copied before it crosses the thread boundary, and all GUI updates happen on the main thread via signals.

Recap

When working with NumPy arrays and QImage across threads, keep these three things in mind:

  1. QImage does not copy NumPy data. It points to the original array's memory buffer. If that buffer changes or is freed, the QImage becomes invalid.

  2. Call .copy() on the QImage before emitting it across threads. This gives the QImage its own memory, independent of the NumPy array.

  3. Always update the GUI from the main thread. Use signals to send data from background workers to slots connected on the main thread, where it's safe to call setPixmap() and other GUI methods.

With these practices in place, you can stream video or display rapidly changing image data without encountering the mysterious crashes that come from shared memory across threads.

For an in-depth guide to building Python GUIs with PyQt6 see my book, Create GUI Applications with Python & Qt6.

09 Sep 2026 6:00am GMT

Python GUIs: Clean up on exit — Stopping threads when closing a PyQt6 application — How to properly shut down background threads and workers when your application window is closed

I'm using QThreadPool and worker threads in my PyQt application. When I click the X button to close the window, the threads keep running in the background. What's the best way to clean everything up on application exit?

This is a very common situation when working with multithreaded PyQt6 applications. You've set up background workers using QThreadPool or QThread, everything runs great - but when you close the window, the application doesn't fully exit. The threads keep going, and you might even see errors in your console.

The solution is to hook into your window's close event and explicitly stop your background work before the window finishes closing. Let's walk through how to do this.

Understanding the problem

When you close a PyQt6 window by clicking the X button (or calling close()), Qt destroys the window and its widgets. But any threads you've started - whether via QThreadPool, QThread, or QRunnable - are managed separately. They don't automatically stop just because the window is gone.

This means your Python process can hang, or you might see tracebacks as threads try to interact with widgets that no longer exist.

Overriding closeEvent

Every QWidget (including QMainWindow) has a method called closeEvent that Qt calls whenever the widget is about to close. By overriding this method, you can run your own cleanup code at exactly the right moment.

Here's a minimal example:

python
from PyQt6.QtWidgets import QMainWindow


class MainWindow(QMainWindow):
    def closeEvent(self, event):
        # Put your cleanup code here
        print("Window is closing - cleaning up!")
        event.accept()

The event parameter is a QCloseEvent. Calling event.accept() tells Qt to go ahead and close the window. If you wanted to cancel the close (for example, to show a "Save changes?" dialog), you would call event.ignore() instead.

Stopping workers on close

If you're managing background workers - for example, through a QThreadPool - you'll want to signal them to stop and then wait for them to finish before allowing the window to close.

Here's how that looks in practice. First, let's set up a simple worker using QRunnable:

python
import time

from PyQt6.QtCore import QRunnable, pyqtSlot, QObject, pyqtSignal


class WorkerSignals(QObject):
    finished = pyqtSignal()


class Worker(QRunnable):
    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.is_running = True

    @pyqtSlot()
    def run(self):
        while self.is_running:
            print("Worker is working...")
            time.sleep(1)
        print("Worker stopped.")
        self.signals.finished.emit()

    def stop(self):
        self.is_running = False

The worker runs in a loop, checking self.is_running on each iteration. When stop() is called, it sets the flag to False, and the loop exits on the next check.

To stop QRunnable objects you need to use this flag-watching approach.

If your runnable doesn't have a loop, and is instead doing a long series of processing steps, you instead will need check the flag state multiple times during that code and return or raise to exit the runner.

The exit can only happen at the points where the flag is checked.

To avoid multiple if checks in the code, an alternative is to have a check method that raises an exception for the stop state. For example:

python
    def maybe_stop(self):
        if not self.is_running:
            raise Exception("Worker stopped.")

You can then use this as follows:

python
import time

from PyQt6.QtCore import QRunnable, pyqtSlot, QObject, pyqtSignal


class WorkerSignals(QObject):
    finished = pyqtSignal()


class Worker(QRunnable):
    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.is_running = True

    @pyqtSlot()
    def run(self):
        print("Worker is working...")
        self.maybe_stop()
        time.sleep(5) # <- do some work.
        self.maybe_stop()
        time.sleep(5) # <- do some more work.
        self.maybe_stop()
        time.sleep(5) # <- do some more work.
        self.maybe_stop()
        time.sleep(5) # <- do some more work.
        self.maybe_stop()
        time.sleep(5) # <- do some more work.
        # <- no point stopping now.
        print("Worker stopped.")
        self.signals.finished.emit()

    def stop(self):
        self.is_running = False

    def maybe_stop(self):
        if not self.is_running:
            raise Exception("Worker stopped.")

We'll not use this approach in our example here, since for testing purposes it is better to have a worker that doesn't stop. But you may find it useful in your own code.

Now let's put together a QMainWindow that starts a worker and cleans it up on close:

python
import sys
import time

from PyQt6.QtCore import QRunnable, QThreadPool, pyqtSlot, QObject, pyqtSignal
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel


class WorkerSignals(QObject):
    finished = pyqtSignal()


class Worker(QRunnable):
    def __init__(self):
        super().__init__()
        self.signals = WorkerSignals()
        self.is_running = True

    @pyqtSlot()
    def run(self):
        while self.is_running:
            print("Worker is working...")
            time.sleep(1)
        print("Worker stopped.")
        self.signals.finished.emit()

    def stop(self):
        self.is_running = False


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Thread Cleanup Example")

        label = QLabel("Close this window to stop the worker.")
        self.setCentralWidget(label)

        self.threadpool = QThreadPool()
        self.workers = []

        # Start a background worker.
        worker = Worker()
        self.workers.append(worker)
        self.threadpool.start(worker)

    def closeEvent(self, event):
        # Signal all workers to stop.
        for worker in self.workers:
            worker.stop()

        # Wait for all threads in the pool to finish.
        self.threadpool.waitForDone()
        print("All workers stopped. Closing application.")
        event.accept()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

When you run this and close the window, you'll see the worker print its "stopped" message, and the application exits cleanly.

Let's look at what's happening in closeEvent:

First, we loop through all our tracked workers and call stop() on each one, setting the flag that tells them to finish. Then we call self.threadpool.waitForDone(), which blocks until every runnable in the pool has completed. This ensures we don't pull the rug out from under a running thread. Finally, we call event.accept() to let the window close.

Managing multiple worker groups

If your application has different categories of workers - say, one group handling camera feeds and another running inference engines - you can keep separate lists and stop them all in closeEvent:

python
def closeEvent(self, event):
    for worker in self.feed_workers:
        worker.stop()
    for worker in self.engine_workers:
        worker.stop()

    self.threadpool.waitForDone()
    event.accept()

The same principle applies: signal every worker to stop, then wait for the thread pool to drain.

Ensuring a clean exit with sys.exit

You might notice that even after the window closes, the Python process occasionally doesn't exit cleanly. This usually happens when sys.exit() isn't receiving the application's return code properly.

The standard way to launch and exit a PyQt6 application is:

python
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

app.exec() starts the Qt event loop and returns an exit code (an integer) when the loop ends. Passing that code to sys.exit() ensures Python terminates with the correct status.

If you skip sys.exit() and just call app.exec(), Python may not tear down all its resources properly - particularly if threads or other objects are still being referenced. Wrapping it in sys.exit() triggers a proper SystemExit exception, which gives Python the chance to clean everything up.

Complete working example

Here's the full working example:

python
import sys
import time

from PyQt6.QtCore import QRunnable, QThreadPool, pyqtSlot, QObject, pyqtSignal
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel


class WorkerSignals(QObject):
    finished = pyqtSignal()


class Worker(QRunnable):
    def __init__(self, worker_id):
        super().__init__()
        self.worker_id = worker_id
        self.signals = WorkerSignals()
        self.is_running = True

    @pyqtSlot()
    def run(self):
        while self.is_running:
            print(f"Worker {self.worker_id} is working...")
            time.sleep(1)
        print(f"Worker {self.worker_id} stopped.")
        self.signals.finished.emit()

    def stop(self):
        self.is_running = False


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Thread Cleanup on Exit")
        self.resize(400, 200)

        label = QLabel("Close this window to stop all workers.")
        label.setMargin(20)
        self.setCentralWidget(label)

        self.threadpool = QThreadPool()
        self.workers = []

        # Start a few background workers.
        for i in range(3):
            worker = Worker(worker_id=i)
            self.workers.append(worker)
            self.threadpool.start(worker)

    def closeEvent(self, event):
        print("Close event received. Stopping workers...")

        for worker in self.workers:
            worker.stop()

        self.threadpool.waitForDone()
        print("All workers stopped. Goodbye!")
        event.accept()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

When you run this, you'll see three workers printing messages to the console. Close the window, and you'll see each worker confirm it has stopped before the application exits.

Summary

This pattern works well for any PyQt6 application with background threads, whether you're processing data, handling network requests, or running live camera feeds. Once you have it in place, your application will shut down gracefully every time. For a complete introduction to using QThreadPool and QRunnable for multithreading in PyQt6, see our Multithreading PyQt6 applications with QThreadPool tutorial. You may also find our guides on signals and slots and creating your first PyQt6 window helpful as you build out your application.

For an in-depth guide to building Python GUIs with PyQt6 see my book, Create GUI Applications with Python & Qt6.

09 Sep 2026 6:00am GMT

08 Sep 2026

feedDjango community aggregator: Community blog posts

Coding tactics: the series

Over the summer I published a series on coding tactics: the everyday craft of ifs, loops, and the reasoning behind them. Eight posts, one thesis, best read in order. This is the map.

Coding tactics: the series

08 Sep 2026 10:00am GMT

06 Sep 2026

feedPlanet Twisted

Glyph Lefkowitz: ... but what about video games?

I get asked this rhetorical question a lot, in various forms:

Sure, datacenters might use a lot of energy, but you don't have to use a hosted frontier model to do software development. What if I just run a local open-weights model to do some coding, with an open-source coding agent? Video games also use my GPU. Is local model development any worse than playing a video game?

So I want to write down my comprehensive answer to this: Yes, using an LLM to write some code is worse than playing a video game, for a few reasons.

Video Games Are Interactive, LLMs Are Batch Jobs

Video games use compute to respond to human input. You are using your GPU while you are looking at a screen, displaying an image. When you are done playing, you shut off the game, and your computer goes back to idle. It's much less energy. By contrast, agentic loops with evals (the only kind of "AI" that is meaningfully any good at coding) are running hot, for days. To use the most recent example of such a thing, a very rough first sketch of an implementation of a Windows graphics API backend to help port a paint program to other platforms, it took 3 weeks of Claude time, "day and night". Do you play a lot of video games for 500 hours to make it past the tutorial level, while also using other computers for other things, as well as the rest of your carbon footprint?

Video Games Need Development, LLMs Need Training

Video games use compute to respond to human input during development, too. Your game has to be made, but your LLM has to be trained. LLMs use a historically extreme amount of power, probably using more than the entire Internet, but it's kind of hard to say. Still, it seems a reasonable estimate to within several orders of magnitude that even over a multi-year project with hundreds of developers, the power used to develop an individual video game is nowhere close to training even a small LLM.

This is true even for local models. OpenAI has openly claimed that DeepSeek "stole its intellectual property", and I have heard grumblings that none of the open-weights generalist models could realistically exist without the massive lift that the frontier labs are doing with their training, in various other ways too. Secrecy throughout the industry makes this kind of impossible to understand rigorously, but it seems fair to say that you are partially culpable for all that famously energy-intensive frontier lab training if you're using a local model.

And They Keep Needing Training

You also can't dismiss this as a sunk cost, because in order to stay current with industry developments, models need to be updated with new information from the rest of the world, which means that you need to keep training them. Beyond the energy for your own use, if you want a real-life agentic workflow that actually does useful stuff, practically speaking you would still need to update your local models over and over again, at least once every few months, which means you would be incentivizing continued energy consumption by whoever was doing that training for you, including the energy cost of scraping.

Let's Be Real Here, You Aren't Actually Using A Local Model

This question is a hypothetical thought experiment. Despite synthetic benchmarks that keep showing there isn't much difference between open weight and frontier models, nobody's actually using local models for much of anything beyond sharing those talking points. Depending on which benchmark you're looking at, maybe it's good enough or maybe it's worse.

As an inveterate AI hater, all these systems seem pretty bad to me, but it seems that people who find them useful tend to subjectively believe the frontier models are worth the premium, and that's what they're actually using. Once you have accepted that it is OK to use LLMs for coding at all, it seems like a very quick slippery slope on down to "we'll go ahead and use the frontier models for now anyway, but we could be ethically better in the future by switching to an open weights one, that option is always available".

There's A Reason We Have Data Centers

Devolving power usage to local LLMs might be good to make users responsible for their costs and decrease the impacts to communities that are physically next to huge concentrations of power utilization, not to mention generation. However, there's a reason that it makes sense for the providers to build these giant facilities: economies of scale reduce total power consumption, they don't increase it. If you do all the same stuff with a local model that they have to do in hosted environments, it will probably take more power, even though you will be incentivized to do different stuff. This incentive to "do different stuff" is why although local models can hypothetically hold their own against the frontier labs for some tasks, when people or businesses take their inference costs in-house they often find that it's too painful and move back to hosted LLMs.

There Are Problems Other Than Power

These are subjects for a different post, but you have to consider a lot of other externalities: AI psychosis, de-skilling, comprehension debt, cultivating a dependency, introducing security defects, limiting your design space based on what LLMs can understand, context rot, wasting time on invalid solutions, introducing unpredictability into your workflows. You still have to consider the total cost benefit ratio.

To Sum Up

Local LLMs might alleviate some of the harms from using the hosted frontier providers. There are fewer privacy concerns, you can measure your power utilization and be more directly responsible for it, you can build interfaces with affordances that are less oriented towards addiction and dependency than the major frontier labs' harnesses.

But they're not automatically "the same as playing a video game" just because they can use the same GPU.

Acknowledgments

Thank you to my patrons who are supporting my writing on this blog. If you like what you've read here and you'd like to read more of it, or you'd like to support my various open-source endeavors, you can support my work as a sponsor!

06 Sep 2026 10:57pm GMT

04 Sep 2026

feedDjango community aggregator: Community blog posts

Issue 353: DjangoCon US Recaps Galore!

News

Django Developers Survey 2026 results

The fifth annual survey run with JetBrains is out, with the full report, infographics, and a companion writeup titled "The State of Django 2026: Boring is so back."

Help test Python 3.15!

Python release manager Hugo van Kemende kindly requests you add 3.15 and allow-prereleases: true to your GitHub Actions matrix and publish wheels before the October 1 release.


Releases

Django bugfix release issued: 6.1.1

Twelve fixes, nearly all of them 6.1 regressions: admin changelist search crashes, ModelAdmin.list_display traversing multiple relations, __in returning empty querysets, and DecimalField without precision on SQLite.

Python 3.15.0 candidate 2 is here!

The last planned release candidate, carrying 144 bugfixes from 76 contributors since rc1, ahead of the October 1 final.


Django Software Foundation

DEP 0019: Technical Governance for Django

Now accepted, DEP 19 supersedes DEP 10 and DEP 12 as Django's single technical governance document, and trades hard eligibility rules for eight qualitative traits that Steering Council candidates should show three or more of. The five-member council keeps binding authority over technical decisions, with elections triggered by the final feature release of a major release series, a drop below three elected members, or a council vote.

DSF member of the month - Benjamin Balder Bach

The django-money maintainer and Django Day Copenhagen organizer on closing the distance between developers and the people who use what they build.


Djangonaut Space News

Djangonaut Space - Session 7 Accepting Applications

Applications for the eight-week mentorship program close September 6 Anywhere on Earth, with the session starting October 12.


Python Software Foundation

The 2026 PSF Board Election is Open!

Eligible members can approve up to 17 candidates for four open seats, and ballots cannot be changed once cast, so read the nominee statements before voting closes September 15 at 2:00 pm UTC.

Inaugural Python Packaging Council Election: Voting is now open!

The first Packaging Council election is open to members who affirmed their intent to vote, and closes at the same September 15 deadline.

Metadata requests no longer tracked in PyPI download counts

PyPI now counts only .whl, .tar.gz, and .zip requests, so BigQuery data breaks permanently at 2026-08-24: about 39% of urllib3's earlier counts turned out to be metadata and other non-distribution files.


Wagtail CMS News

Our DjangoConUS 2026 photo album 📷

Meagen Voss shares photos from Chicago rather than a talk recap, including her first main-stage talk on Wagtail's approach to AI.


Updates to Django

Today, "Updates to Django" is presented by Raffaella from Djangonaut Space! 🚀

Last week we had 5 pull requests merged into Django by 5 different contributors - including 2 first-time contributors! Congratulations to Iaroslav and Tyler Russin for having their first commits merged into Django - welcome on board!

News in Django 6.1:

Playwright is replacing Selenium for integration tests 🎉


Django Fellow Reports

Django Fellow Report - Jacob

I had a rejuvenating week attending, presenting at, and sprinting during DjangoCon US. I'm still relatively new to this community, so I'm still allowed to be impressed with everyone's gracious and welcoming attitudes. A smattering of things falling under the usual categories this week.

Django Fellow Report - Sarah

Was in Chicago (🌬️ 🏙️ 🌭 🇺🇸) for DjangoCon US 🎉. It was a fantastic conference and a lovely city. Delivered a keynote which went "good enough" and my baby boy managed with the jetlag reasonably well 😀. Came away from the conference with a few ideas and energy from engaging with our community

Django Fellow Report - Natalia

A week of holidays 🏖️ 👡 🍦 followed by a week of DjangoCon US! 🚆 ☀️ 👥 🎤


Sponsored

Until September 10, receive 30% off all new or renewal licenses, with 100% of the proceeds going directly to the Django Software Foundation.


Articles

Why We Started Building on the Django 6.1 Alpha

Divio started building on Django 6.1 at the first alpha, months before the August release. Here's why they picked the pre-release and what they found while testing it every day.

A Dolly Parton Developer

Following Rikki Endsley's "Willie Nelson developer," Trey Hunner makes the case for Dolly Parton as the model: know your rights (she refused to hand over publishing on "I Will Always Love You" when Elvis's team demanded half), exit with grace (she paid Porter Wagoner $1 million to leave his show and kept the friendship), and write the next one. She recorded close to 1,000 songs and wrote thousands more, which is a better target than being a rockstar.

Store lists in a single Django column without joins?

After a decade of development, version 1.0.0 of django-select-multiple-field is here, bringing full support for modern Python and Django versions to store multiple choices in a single database column without extra join tables.

Make Your Django Application Editable

The CMS doesn't need to own your data to make it editable.

htmx and Django LiveView, side by side

Seven worked cases showing where stateless htmx requests and LiveView's persistent WebSocket diverge, with the conclusion that they are complementary rather than interchangeable.

Nifty Django Feature: Use Index for Custom Migration Operations

Override create_sql() and remove_sql() on a models.Index subclass and arbitrary table-level SQL rides along in Meta.indexes, managed by migrations for free.

Django and deployments

A proposed manage.py deploy namespace of lower-level commands that start by printing their expected inputs and outputs, leaving the actual automation to packages and plugins.

Agents All the Way Down

The annotated script of Josh Thomas's DjangoCon US talk on how AI coding agents changed the way he writes Django, written to land for skeptics and true believers alike.

Postgres 19: How Our Advice Has Changed Since...

JIT is off by default, LZ4 replaces pglz for TOAST, and async I/O means the old "an index always beats a parallel sequential scan" assumption is worth re-testing.

"Premature" optimization

The full Knuth quote licenses optimizing the critical 3%, and dropping "small" from "small efficiencies" turned it into a blanket excuse to skip the design work that is cheapest to do up front.


DjangoCon US Recaps

Yes, a standalone category since so many posts on it this week!

DjangoCon US 2026 Recap - Jonathan Peacher

Jonathan Peacher's notes on attending the conference this year in Chicago, highlighting various talks and projects.

My Time at DjangoCon US 2026 - Jason Judkins

Jason Judkins transcribed the talks so he could go back over them, and this recap is the trailer for a longer per-talk series. He picks out a theme running through Paolo Melchiorre's UUID history, Drishti Jain's GeoDjango talk, and Abigail Gbadago's polyglot persistence talk: push the work down a layer, because the database usually knows how to do it better than you do. AI turned up in nearly every talk, with almost nobody uncritical about it.

DjangoCon US 2026 Recap - Tim Schilling

Tim Schilling's fifth DjangoCon, spent chairing sprints with Kudzayi Bamhare, working on Django Simple Deploy with Colin Copeland, and meeting Djangonaut Space members in person for the first time.

My DjangoCon US 2026 - Paolo Melchiorre

Paolo Melchiorre on giving "The Django UUID Story," staffing the DSF booth, and fielding questions at the DSF members open space.

DjangoCon US 2026 - Dwayne McDaniel

Dwayne McDaniel's recap runs talk by talk: Karen Tracey on Django 6's background tasks, CSP support, and template partials, Natalia Bidart on keeping templates the source of truth with HTMX, and Elizabeth Christensen on UUIDv7, graph queries, and OAuth 2.0 in PostgreSQL 18 and 19. His through line is that frameworks, databases, and browsers keep absorbing work that used to need extra layers, with Kasey Kelly's 16,000-line AI-generated frontend file as the cautionary case.


Events

Django On the Med

September 23, 2026 in Pescara, Italy 🇮🇹.

Django Day Copenhagen 2026

October 2, 2026 in Copenhagen 🇩🇰.


Django Job Board

Two new listings this week, plus the DSF still looking for its first Executive Director.

Machine Learning Engineer (Hybrid) at Provision 🆕

Django Developer at The Cruise Brothers 🆕

Full Stack Software Engineer (Hybrid) at Provision

Executive Director at Django Software Foundation

AI-Assisted Software Engineer, Web Applications at Logical Media Group


Projects

django-danceschool/django-danceschool

Django CMS project with comprehensive features for running a partnered social dance school.

p-r-a-v-i-n/django-fast-multipart

An experimental Rust-backed multipart parser that plugs into Django's parser extension point, so upload handlers, request limits, request.POST, and request.FILES all keep working as they do now. Requires CPython 3.12 or later and Django 6.1, with prebuilt wheels for Linux, macOS, and Windows.

04 Sep 2026 3:00pm GMT

02 Sep 2026

feedDjango community aggregator: Community blog posts

Django and deployments

I have been pondering the wider deployment space in Django for a while and from various angles. This includes my released package django-prodserver but also wondering if the DSF could provide hosting as a small scale commercial operation or what via alternatives I could offer in hosting for Django specifically. Then also I have considered what the wider API in Django could be for deployments.

These thoughts come at a good time, Will Vincent has done two talks on deploying python projects this year and I think his talks would serve as a great theoretical starting point to ensure we cover 90% of what is required. Then after DjangoCon US last week, Paolo made toot suggesting it's time for a deploy command. That toot triggered two things, first a memory of the chats I had in Athens this year and DjangoCon Europe and that I had been meaning to write about this topic for a while.

First let's consider the high level conceptual stages when deploying a project:

  1. Prepare the overall environment - signing up for an account, creating a project or just booting up a VPS
  2. Prepare Django and it's settings - these are changes made to the project repository
  3. Get the Django project from source control to the environment
  4. Do the first time setup - ideally this would be idempotent.
  5. Start the production process
  6. Doing a second deployment - because code always changes and then repeat step 5.

From this list, I think a single managed.py deploy might be too much magical to begin with, but I do think it's possible eventually. I'm thinking it's more likely deploy to be a command that stitches together several lower level commands and each of those commands correspond to a step in the above list. So we could have something like:

  1. manage.py init_deploy_env
  2. manage.py productionize
  3. manage.py deploy_project --first
  4. manage.py initialize --production
  5. manage.py prodserver web and manage.py worker
  6. manage.py deploy_project

A couple of very important points, first those names are simply examples for this post to communicate the idea and perhaps it would be best to have them all within a namespace of deploy, so manage.py deploy productionize etc.

Second and most importantly, I am very aware of the numerous possible combinations that exist when it comes to how a project can be deployed today and I am very much NOT suggesting Django support any of them. What I am suggesting is that we focus on the common API inside Django and we have packages and plugins like Eric has with django-simple-deploy. My approach here would be create an API that explicitly does nothing but simply prints expected inputs and outputs from each step. We can then start to automate the parts worth automating in a package, which may get us to a single deploy command.

Let me know your thoughts! As the maintainer of django-prodserver I have a vested interest in this space! :D

PS It's worth noting that there have been years of packages that have done similar things and we should use as reference, django-production is one such package or dj-lite for sqlite configuration in production.

02 Sep 2026 5:00am GMT

06 Aug 2026

feedPlanet Twisted

Hynek Schlawack: Production-ready Python Docker Containers with uv

Starting with 0.3.0, Astral's uv brought many great features, including support for cross-platform lock files uv.lock. Together with subsequent fixes, it has become Python's finest workflow tool for my (non-scientific) use cases. Here's how I build production-ready containers, as fast as possible.

06 Aug 2026 12:00am GMT

23 Jun 2026

feedPlanet Twisted

Glyph Lefkowitz: Adversarial Communication

As I have discussed in previous posts, "AIs" can make mistakes. In fact, they do make mistakes, and their mistake-making patterns are such that where and how they will make mistakes is both uncertain and constantly changing.

Thus, in any scenario where you want to attempt to make "productive" use of "AI", you must have a system in place for checking every result. Not checking some results; checking every result. If each result might have a consequence for you (and if it didn't have a consequence, why bother automating it?) and you cannot predict in advance which kinds of results will need verification, then verification is always required.

The verification often ends up being just as expensive as doing the work in the first place, which means that if you want your usage of "AI" to be personally profitable, you have to find someone else to externalize the cost of verification onto. This person becomes your adversary, and, if you are successful, your "AI's" victim.

The Ladder-Climber And Their Reverse-Centaur Rungs

One way that this constellation of facts can straightforwardly assemble themselves into a dystopian nightmare is the phenomenon, described by Cory Doctorow, of the reverse centaur. This is when your employer non-consensually turns you into the verification system. The "AI" does the fun part of initially performing the work, and then you do the boring part where you check if the robot is right and clean up its messes, even if everyone already knows that it would, in aggregate, be cheaper for you to do the work in the first place.

Reverse centaurs can be made from any automation, not only "AI" automation. I think that there is a reason that this term happens to have emerged in the "age of AI", though, and not with earlier automation technologies (even those which were considerably more viscerally horrific). That reason is: the wrongness of "AI" output is not merely a technical feature that must be compensated for, it is a generalized externality.

As I mentioned above, if you are responsible for the entirety of the work, both extruding the "AI" output and checking it, it's usually cheaper to have humans do the entirety of the work to begin with. When humans do the writing directly, we can check as we go, and thus verification doesn't need to be as comprehensive.

When "AI" coding advocates say "code review is the bottleneck", what they are observing is that the LLM is still rolling the dice for each PR, and a human is still necessary to verify that each of those rolls is a winner. But calling this process "code review" is a bit of a misnomer; it's not really "code review" in the traditional sense, it's human understanding.

Before the advent of "AI", the human understanding was implicit in the process of writing the code in the first place1, and the code review was a way of diffusing and extending that understanding. Now that the code can be authored with no initial understanding taking place, that cost has not gone away, it has moved.

Human understanding was always the bottleneck.

However, this is taking a collaborative view of a software project, where satisfying the needs and solving the problems of your customers are the goals. We can see that "AI" is a bad tool to satisfy those goals, because all it's doing is converting the first half of the work, that of understanding the code as you write it, to understanding the agent's output as you read it.

What if, instead, we were to take the view that every software company is a Hobbesian nightmare, red in tooth and claw? In this view, the only goal of a software project is for the individual developers to make their promo cycles and get their bonuses. Given that there is only a certain amount of money to go around, this is a zero-sum game where each programmer wants to look more productive than their colleagues.

Pretty much every organization finds it easy to reward "productivity" as expressed by lines of code emitted, but the benefits of doing thorough and thoughtful design, analysis, and code review very difficult to reward. In this world, an LLM is an invaluable tool for the sociopathic ladder-climber, particularly if your legacy organization is still structuring their workflows as if the person prompting the bot is "writing" the code, and then they get to foist off the act of "reviewing" the code onto someone else.

Here, the prompter effectively externalizes the cost of the LLM's failures but internalizes any benefits. The prompter will vibe-code a big feature, so large that the assigned reviewer can't possibly comprehend it all effectively. When this happens, the reviewer will, eventually, be pressured to approve it, even if they can try to spot a few problems along the way. The reviewer has their own work to get back to, after all, the obligation to review the prompter's (read: the bot's) code is a drain on their time that they are not going to get rewarded for.

If this feature is a big success, the prompter gets a promotion. If it causes a big issue, well, the reviewer must not have been careful enough.

This is why LLMs are "good for coding", and also why their biggest promoters keep having outages.

The Generative Gish Galloper

Coding is the biggest "success story" of this type of adversarial communication, but it is by far not the only instance of such a thing. LLMs create a new form of leverage that can turn Brandolini's law from a linear advantage into an exponential one. If you are engaged in a political debate where you want to overwhelm the other side in nonsense, an LLM can generate bullshit faster than it is physically possible for a human being to type, let alone respond thoughtfully. There is an asymmetry to the utility of this weapon as well: only one side of the political spectrum wants to flood the zone and destroy trust in institutions and the concept of truth. There's a good reason that the fascists love it.

Straightforward Spam and Fraud

This is kind of obvious, but LLMs can generate lightly-customized, plausible-looking text much more quickly than any human being. This facilitates their use in fraud, spam, and scams. In a spamming or fraudulent interaction, once again, the costs are externalized onto the victim: the recipient of a spam message has to do all the work of "checking" the LLM's output. Spammers already expect very low hit rates from boilerplate, and if the LLM can increase those percentages from 1% to 5% the technology will pay for itself; they don't need anything like reliable accuracy.

Customer "Support"

If you have any kind of commercial relationship with a company, I probably don't even need to mention this: customer "support" bots are a misery. Everybody knows it at this point. But customer support is usually conceptualized by businesses as an adversarial interaction, because it is a cost center. They maintain internal metrics on time-to-resolution and try to optimize them. Implicitly, this creates a dynamic where the goal of the customer service agent's job is not to solve your problem, but to emit noise that will cause you to think your problem is resolved, or to give up, as fast as possible. Unsurprisingly, LLMs can emit this noise faster than humans can, getting those customers off the phone. But those customers will remember those interactions, and the story outside the TTR metrics is horrible.

Similarly to the situation in software development, LLMs can look very good on paper for customer support, but mostly what they are doing is illuminating the problems with the industry's existing metrics, by turning "winning the metrics battle against the customer" into a more obvious and immediate defeat for the company's long term reputation.

"Education"

In 2026 it is sadly a fact of life that students cheat all the time using "AI", and that this cheating is very successful, in that the teachers find it very hard to detect.

LLMs are great for cheating on schoolwork because the student is externalizing the work of the checking onto the teachers, who are often starting at a disadvantage to begin with, at least in the US.

My view is that this is happening because of a divergence in the way that students vs. teachers (or, more accurately, "the broader educational system") view grading.

When a student is asked to write an essay, the teachers see the effort as both intrinsically worthwhile for the student, as well as useful as a pedagogical tool to evaluate and react to the student's progress. The student, by contrast, sees a stumbling block designed to knock them off the path to success and into a permanent underclass. It is no wonder that the student sees "AI" as useful to their own goals and has no compunction about deploying it.

There is a bitter irony that the ability to understand the inherent value of actually writing the essay on their own is the sort of thing that students can really only learn by writing a bunch of essays. There's no way that I can think of which makes the benefit legible as long as a shortcut is available.

The net effect here is a downward spiral, where the already-wobbling educational system is sustaining an attack that it doesn't have the resources to recover from. The individual students' attacks against their teachers and their schools' grading systems might appear to momentarily succeed, but they will win the battle and lose the war.

Spamming "For Good"?

Usually when we talk about someone unilaterally choosing to enter into an adversarial relationship, that's an "attack" and for good reasons we have a negative impression of the attacker. However, I would be remiss if I did not point out that there are some cases where the relationship was already adversarial; just because you're the attacker doesn't mean that you are evil.

For example we might imagine use-cases like automatically filing appeals for prior authorizations against health insurance. It's relatively well-known at this point that the main way for-profit insurers maintain their margins is by denying claims right up to the line of the policies themselves being fraud, so using a spamming tool to fight them might be entirely justifiable2 in that case.

Similarly, using an LLM could be justified in a fight against a company refusing to honor a warranty. One could imagine using an LLM to immediately generate replies and escalations.

However, even in imagined cases like these, the underlying problem is that the insurers and the vendors already have a tremendous amount of structural power, so it is more likely that they will have the advantage in deploying a communications weapon like an LLM, as well as enacting policies to simply ignore any LLM-based communication that you might submit. Worse, if these strategies were to become widespread, they might provide an excuse to reject any communications by feeding them into an unreliable "LLM detector" and issuing an automated "computer says no" even to hand-written correspondence.

It is also worth stressing that these cases are imagined, as compared to the very real coworker-abuse, spam, scam, fraud, and disinformation campaigns being waged in real life today.

Therefore, while legitimate uses might exist, it's hard to imagine that there's anywhere they would be genuinely valuable and sustainable. In the best case "AI" will provide a temporary advantage for underdogs that will provoke an arms race which the resource-advantaged adversaries will win in the long run, in the worst case the arms race itself will cement permanent structural change that will make things worse.

"Search" By Stealing

Most of the adversarial utility of "AI" is on the "write" side, since write-amplification is more obviously aggressive than reading. But the "read" side of LLMs - summarization and question-answering - can be a form of attack as well.

To begin with, the act of reading itself is currently enormously destructive, but that's arguably not a fundamental aspect of this technology. They could set reasonable rate-limits and respect things like robots.txt, as search engines have for decades now. They could also refrain from committing criminal levels of copyright infringement. But, today, using "AI" tools does suborn this sort of out-of-control crawling.

More insidiously, consider the scenario described in this YouTube video. The LTT Bros decided to try Linux again, and in the course of so doing, they had problems. When trying to solve these problems, they were faced with a choice: they could consult Reddit, or they could ask an LLM. Asking an LLM would "gaslight the heck out of" them, but they still found it preferable, because they would at least get an answer without getting yelled at.

Initially this sounds great. But it also means that you want to extract knowledge from a community, while mechanically eliding any values or norms that the community may want to impart as part of offering that knowledge. As someone who spent many years in a community tech support role, this is worrying. Many requests for support are people asking how to do things that will momentarily solve a superficial problem but create a long-term reliability problem or even an immediate security risk, that the question-asker doesn't want to hear about. Consider the question "I'm tired of entering my password so much, how do I make it so my laptop unlocks automatically". An obsequious chatbot will helpfully tell you how to do this without pushback.

But, this is also a sort of ethically murky area. The Linux community is somewhat famously, for many years now, a toxic cesspool of general hostility, misogyny, etc. It is certainly a good thing that people can get access to this knowledge without subjecting themselves to abuse. But it also means that the people with the power and the privilege to change the community for the better can just quietly withdraw, rather than fixing the problems. It also means that the positive elements of culture cannot be transmitted, and people will have no opportunity to learn about unknown unknowns.

In this case, the "adversarial" communication is with society. The thing that using an LLM for search lets you do is withdraw from society and avoid forming any personal connections. There are some personal connections which are painful and annoying, and so that can feel like a momentary balm. But the need to make connections in general is, like, the concept of society itself.

Who Am I Hurting?

LLMs are good at adversarial communication. They are so good at it, relative to their other benefits, that they will tend to make communications adversarial if you are not remaining vigilant about the possibility that it might do so. My request to you, dear reader, if you are going to use such tools, is to always ask yourself, "who might I be hurting, if I use an LLM for this?"

If you're using an "AI", who is its adversary? If you haven't given it one yet, who might the "AI" turn into an adversary? Who might you overwhelm with an asymmetric amount of output, or, if you're receiving information and not sending it, who are you taking that information from without consulting?

Figure out the answers to these questions and conduct yourself accordingly; the answer might be "yourself".

Acknowledgments

Thank you to my patrons who are supporting my writing on this blog. If you like what you've read here and you'd like to read more of it, or you'd like to support my various open-source endeavors, you can support my work as a sponsor!


  1. One of the reasons that software developers tend to prefer greenfield development is that when you are given a blank page, you can project your own specific understanding onto it. You can structure the codebase in a way that works for your brain, down to the variable naming conventions and the module layouts. LLM-assisted development makes everything into instant brownfield work, which makes developers instantly miserable; even those who are excited about the technology will frequently complain about how it feels like their agency has been stolen and their joy in the work has been diminished. But I digress.

  2. Modulo the massive amount of other externalities involved in using LLMs, of course, but I don't have the time or energy to get into those here.

23 Jun 2026 8:06pm GMT