Skip to content

gzip raising exception when closing with buffer backed by BytesIO #129726

Description

@xrmx

Bug report

Bug description:

Hello,

the following snippet raises an exception in Python 3.13.2 while it's fine with Python 3.12.

import io
import gzip

def foo():
    buffer = gzip.GzipFile(fileobj=io.BytesIO(), mode="w")

foo()

Running this with python3.12 is silent, with Python 3.13.2 instead:

Exception ignored in: <gzip on 0x7fa4fd99c550>
Traceback (most recent call last):
  File "/usr/lib/python3.13/gzip.py", line 359, in close
    fileobj.write(self.compress.flush())

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Feb 6, 2025
  2. changed the title [-]gzip raising exception when closing empty buffer backed by bytesio[/-] [+]gzip raising exception when closing empty buffer backed by BytesIO[/+] on Feb 6, 2025
  3. changed the title [-]gzip raising exception when closing empty buffer backed by BytesIO[/-] [+]gzip raising exception when closing with buffer backed by BytesIO[/+] on Feb 7, 2025
  4. added
    stdlibStandard Library Python modules in the Lib/ directory
    and removed on Feb 7, 2025
  5. cmaloney commented on Feb 9, 2025

    @cmaloney
    Contributor

    Explicit buffer.close() and del buffer in the method make the error go away, so I think this has to do with ordering of cleanup of references when buffer is actually deallocated. Moving to with gzip.GzipFile ... as buffer: also doesn't error. The issue seems to be that the io.BytesIO is cleaned up before the gzip.GzipFile.

    The same behavior occurs with an explicit reference to the io.BytesIO:

    def explicit_reference():
        bio = io.BytesIO()
        buffer = gzip.GzipFile(fileobj=bio, mode="w")

    Can work around it so no longer error at least by updating GzipFile.close() to check if self.fileobj.closed and fast-path exiting (See: https://github.com/python/cpython/compare/main...cmaloney:cpython:exp/gzip?expand=0). Not sure that is the right approach though vs. getting the objects to destruct in the right order.

  6. xrmx commented on Feb 9, 2025

    @xrmx
    ContributorAuthor

    Explicit buffer.close() and del buffer in the method make the error go away, so I think this has to do with ordering of cleanup of references when buffer is actually deallocated. Moving to with gzip.GzipFile ... as buffer: also doesn't error.

    Thanks for testing these, I cannot use the context manager but I may be able to close the buffer explictly.

    The issue seems to be that the io.BytesIO is cleaned up before the gzip.GzipFile.

    Yeah, that is my impression too

    The same behavior occurs with an explicit reference to the io.BytesIO:

    def explicit_reference():
    bio = io.BytesIO()
    buffer = gzip.GzipFile(fileobj=bio, mode="w")

    Can work around it so no longer error at least by updating GzipFile.close() to check if self.fileobj.closed and fast-path exiting (See: https://github.com/python/cpython/compare/main...cmaloney:cpython:exp/gzip?expand=0). Not sure that is the right approach though vs. getting the objects to destruct in the right order.

    Yeah would be nice to keep the code working instead of not just crashing :) The gzip code should already have references to the fileobj so to me is surprising it gets deallocated while we are in in GzipFile.close.

  7. xrmx commented on Feb 10, 2025

    @xrmx
    ContributorAuthor

    So my original code when getting the gzipped content is this so am not sure I can workaround this other than stop using GzipFile.

        fileobj = buffer.fileobj  # get a reference to the fileobj before closing the gzip file
        buffer.close()
    
        data = fileobj.getbuffer()
    

    Irony of the comment 😅

    The documentation explicitly says that calling GzipFile's close() does not close the fileobj, so I guess it also means it should not be freed:

    Calling a GzipFile object’s close() method does not close fileobj, since you might wish to append more material after the compressed data.
    This also allows you to pass an io.BytesIO object opened for writing as fileobj, and retrieve the resulting memory buffer using the io.BytesIO object’s getvalue() method.
    
  8. cmaloney commented on Feb 10, 2025

    @cmaloney
    Contributor

    I still haven't found what is causing the fileobj to be closed... The line which is causing the exception print was added to 3.12 in https://github.com/python/cpython/pull/105920/files it looks like to fix #105808

  9. xrmx commented on Feb 10, 2025

    @xrmx
    ContributorAuthor

    @cmaloney the one you linked touches flush, here is crashing in close. AFAICS the only commit that touches close (and coincidentally the gc 😅 ) that is in 3.13 but not in 3.12 is b52fc70#diff-ad9b54ac8ef847cbb11fb0550f7e8cede55b1d92de15899e0a885a94a124838a

  10. cmaloney commented on Feb 10, 2025

    @cmaloney
    Contributor

    I think it's the move to using a BufferedWriter (gh-89550) changed the behavior around flush and close in some unintended ways (https://github.com/python/cpython/blob/main/Modules/_io/bufferedio.c#L543-L598 being one of the parts). That the stream is being marked as "closed", but later gets flushed to. Still investigating / don't have any verifiable answers yet, just hunches.

  11. danifus commented on Feb 11, 2025

    @danifus
    Contributor

    Bisected this issue to #105104

  12. xrmx commented on Feb 11, 2025

    @xrmx
    ContributorAuthor

    Ok so with 3.12 and development mode I see the same warning, don't see the warning in 3.10 and 3.11 though:

    $ python3.13 gzipfoo.py 
    Exception ignored in: <gzip on 0x7f39c7098580>
    Traceback (most recent call last):
      File "/usr/lib/python3.13/gzip.py", line 359, in close
        fileobj.write(self.compress.flush())
    ValueError: I/O operation on closed file.
    $ python3.12 gzipfoo.py 
    $ python3.12 -X dev gzipfoo.py 
    Exception ignored in: <gzip on 0x7f1450669590>
    Traceback (most recent call last):
      File "/usr/lib/python3.12/gzip.py", line 357, in close
        fileobj.write(self.compress.flush())
    ValueError: I/O operation on closed file.
    $ /usr/bin/python3.10 -X dev gzipfoo.py 
    $ /usr/bin/python3.11 -X dev gzipfoo.py
    

    So there is something that changed the behavior in 3.12.

  13. xrmx commented on Feb 12, 2025

    @xrmx
    ContributorAuthor

    @danifus thanks for your bisection. Any chance you can bisect with the -X dev param? I think there are good chances the code was failing already, just not printing the exception.

  14. 4 remaining items

  15. added 2 commits that reference this issue on Feb 28, 2025
  16. vstinner commented on Feb 28, 2025

    @vstinner
    Member

    Fixed by change 7f39137. Backports to 3.12 and 3.13 will follow.

  17. xrmx commented on Feb 28, 2025

    @xrmx
    ContributorAuthor

    Thanks a lot!

  18. vstinner commented on Feb 28, 2025

    @vstinner
    Member

    Maybe GzipFile should emit a ResourceWarning if the file is not closed explicitly to make sure that data are written on disk.

  19. added 2 commits that reference this issue on Feb 28, 2025
  20. cmaloney commented on Feb 28, 2025

    @cmaloney
    Contributor

    I implemented a ResourceWarning on BufferedWriter checking for unwritten data, and just the CPython I/O tests it fires a lot. Even with conditions (only if raw isn't closed, etc) many cases to work through. I think worthwhile to help track down and cleanup inconsistencies at least in CPython itself. Not sure if it's going to be too noisy in general Python ecosystem codebases though.

  21. vstinner commented on Mar 2, 2025

    @vstinner
    Member

    The test suite can be fixed. The risk of losing data justify emitting ResourceWarning.

  22. added a commit that references this issue on Mar 6, 2025
  23. added a commit that references this issue on Mar 8, 2025
  24. added a commit that references this issue on Mar 13, 2025
  25. added a commit that references this issue on Mar 17, 2025
  26. josecsotomorales commented on Apr 9, 2025

    @josecsotomorales

    Hey @xrmx I see that a new CPython version was released, looks like it does include the fix of the issue you reported: https://github.com/python/cpython/releases/tag/v3.13.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions