Django: silence “Exception ignored in ... OutputWrapper”

Shuck this cob out of its wrapper.

You might see this message when running pytest on your Django project:

$ pytest
...
Exception ignored in: <django.core.management.base.OutputWrapper object at 0x108576170>
Traceback (most recent call last):
  File "/.../django/core/management/base.py", line 171, in flush
    self._out.flush()
ValueError: I/O operation on closed file.

The message doesn’t fail tests but reports an unraisable exception that occurred inside Django. This is an exception at a point that Python cannot crash the program. It’s triggered by a bug in Django’s OutputWrapper class, which is used to wrap output in management commands when used in combination with pytest’s output capturing.

The message can appear twice, once for sys.stdout and once for sys.stderr, for each management command tested for an exception, like:

with pytest.raises(CommandError) as excinfo:
    call_command(...)

This message is always visible on Python 3.13+, but it can also appear on older versions when using Python’s development mode, like:

$ python -X dev -m pytest

(I recommend using development mode locally and on CI; it activates several useful features!)

I reported and fixed the underlying issue in Ticket #36056. The commit is scheduled for release in Django 5.2 and will not be backported to older versions. So, until 5.2 is out, your test output can get cluttered with these messages.

Below is a fix that wraps the default sys.unraisablehook to silence these exceptions. Add the code to your root conftest.py—no need to wrap it within any pytest hook function.

import sys

import django
from django.core.management.base import OutputWrapper

# Silence “Exception ignored in ... OutputWrapper”:
# ValueError: I/O operation on closed file.
# https://adamj.eu/tech/2025/01/08/django-silence-exception-ignored-outputwrapper/
# https://code.djangoproject.com/ticket/36056
if django.VERSION < (5, 2):
    orig_unraisablehook = sys.unraisablehook

    def unraisablehook(unraisable):
        if (
            unraisable.exc_type is ValueError
            and unraisable.exc_value is not None
            and unraisable.exc_value.args == ("I/O operation on closed file.",)
            and isinstance(unraisable.object, OutputWrapper)
        ):
            return
        orig_unraisablehook(unraisable)

    sys.unraisablehook = unraisablehook

The fix works by detecting the specific unraisable exception and returning early, or otherwise calling the original hook. The fix is wrapped with a Django 5.2 version check, so it won’t run after you upgrade to the fixed version. If you use my tool django-upgrade, it will automatically remove the whole if statement later when you target it to Django 5.2+.

Fin

May you be blessed with tests that pass silently and smoothly hereafter! 🌽🌽🌽

—Adam


😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: