74

I'm modifying some code to be compatible between Python 2 and Python 3, but have observed a warning in unit test output.

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/case.py:601:
    ResourceWarning: unclosed socket.socket fd=4,
    family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6,
    laddr=('1.1.2.3', 65087), raddr=('5.8.13.21', 8080)

A little research determined this was also happening from popular libraries like requests and boto3.

I could ignore the warning or filter it completely. If was my service, I could set the connection: close header in my response (link).

Here's an example that exhibits the warning in Python 3.6.1:

app.py

import requests

class Service(object):
    def __init__(self):
        self.session = requests.Session()

    def get_info(self):
        uri = 'http://api.stackexchange.com/2.2/info?site=stackoverflow'
        response = self.session.get(uri)
        if response.status_code == 200:
            return response.json()
        else:
            response.raise_for_status()

    def __del__(self):
        self.session.close()

if __name__ == '__main__':
    service = Service()
    print(service.get_info())

test.py

import unittest

class TestService(unittest.TestCase):
    def test_growing(self):
        import app
        service = app.Service()
        res = service.get_info()
        self.assertTrue(res['items'][0]['new_active_users'] > 1)


if __name__ == '__main__':
    unittest.main()

Is there a better / correct way to manage the session so that it gets explicitly closed and not rely on __del__() to result in this sort of warning.

Thanks for any help.

Joey
  • 1,436
  • 2
  • 19
  • 33
j12y
  • 2,112
  • 3
  • 17
  • 22
  • 3
    Make a `close` method for your class that delegates to the underlying resource's `close`? Even better, make your class implement [the context manager](https://docs.python.org/3/glossary.html#term-context-manager) protocol, then use it with the `with` statement (the `__exit__` can just call your `close` method, and the `__enter__` can be a trivial `return self`, so it's not much extra work). – ShadowRanger Jan 09 '18 at 02:52
  • Dont use __del__ as it is not guaranteed that this gets called in he correct order when shutting down. This is the reason that context managers were invented. @ShadowRanger is correct - add __enter__ and __exit__ methods and use the with syntax – Major Eccles Nov 25 '18 at 12:13
  • I got this error after getting a DeprecationWarning (I was using assertEquals instead of assertEqual). – Dave Russell Jul 29 '20 at 01:57

2 Answers2

34

Having the teardown logic in __del__ can make your program incorrect or harder to reason about, because there is no guarantee on when that method will get called, potentially leading to the warning you got. There are a couple of ways to address this:

1) Expose a method to close the session, and call it in the test tearDown

unittest's tearDown method allows you to define some code that will be run after each test. Using this hook to close the session will work even if the test fails or has an exception, which is nice.

app.py

import requests

class Service(object):

    def __init__(self):
        self.session = requests.Session()

    def get_info(self):
        uri = 'http://api.stackexchange.com/2.2/info?site=stackoverflow'
        response = self.session.get(uri)
        if response.status_code == 200:
            return response.json()
        else:
            response.raise_for_status()

    def close(self):
        self.session.close()

if __name__ == '__main__':
    service = Service()
    print(service.get_info())
    service.close()

test.py

import unittest
import app

class TestService(unittest.TestCase):

    def setUp(self):
        self.service = app.Service()
        super().setUp()

    def tearDown(self):
        self.service.close()

    def test_growing(self):
        res = self.service.get_info()
        self.assertTrue(res['items'][0]['new_active_users'] > 1)

if __name__ == '__main__':
    unittest.main()

2) Use a context manager

A context manager is also a very useful way to explicitly define the scope of something. In the previous example, you have to make sure .close() is called correctly at every call site, otherwise your resources will leak. With a context manager, this is handled automatically even if there is an exception within the scope of the context manager.

Building on top of solution 1), you can define extra magic methods (__enter__ and __exit__) so that your class works with the with statement.

Note: The nice thing here is that this code also supports the usage in solution 1), with explicit .close(), which can be useful if a context manager was inconvenient for some reason.

app.py

import requests

class Service(object):

    def __init__(self):
        self.session = requests.Session()

    def __enter__(self):
        return self

    def get_info(self):
        uri = 'http://api.stackexchange.com/2.2/info?site=stackoverflow'
        response = self.session.get(uri)
        if response.status_code == 200:
            return response.json()
        else:
            response.raise_for_status()

    def close(self):
        self.session.close()

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

if __name__ == '__main__':
    with Service() as service:
        print(service.get_info())

test.py

import unittest

import app

class TestService(unittest.TestCase):

    def test_growing(self):
        with app.Service() as service:
            res = service.get_info()
        self.assertTrue(res['items'][0]['new_active_users'] > 1)

if __name__ == '__main__':
    unittest.main()

Depending on what you need, you can use either, or a combination of setUp/tearDown and context manager, and get rid of that warning, plus having more explicit resource management in your code!

Samuel Dion-Girardeau
  • 2,790
  • 1
  • 29
  • 37
  • am i only who still have this error even using context manager? The same if open connection in `setUp()` and close in `tearDown()`. The same using both... – 555Russich May 23 '23 at 20:00
  • @555Russich do you have a minimum reproducible example for that? Maybe the warning is coming from another use of `request.Session`? – Samuel Dion-Girardeau May 24 '23 at 21:30
  • I lied a little, because I'm using similiar asynchronous code to reproduce same error. Specifically `aiohttp`, `unittest.IsolatedAsyncioTestCase`, `async with` context manager etc. Just now created [new question](https://stackoverflow.com/q/76334048/15637940) – 555Russich May 25 '23 at 15:39
24

This is the best solution if you are not much concern about warnings

Just import warnings and add this line where your driver is initiating -

import warnings

warnings.filterwarnings(action="ignore", message="unclosed", category=ResourceWarning)
Uri
  • 2,992
  • 8
  • 43
  • 86
Shivam Bharadwaj
  • 1,864
  • 21
  • 23
  • 2
    In my case adding `warnings.filterwarnings(...` line after `if __name__ == '__main__':` solved the problem. – Comrade Che Jul 24 '19 at 12:40
  • 5
    I had to put the "warnings" line at the top of the first offending function for each class. – JJones Nov 02 '19 at 14:55
  • 1
    For me it worked in the classes and didn't work if the "warnings" line is at the top of the file. – Uri Sep 15 '20 at 06:41
  • 1
    For me, it worked only if placed it in my unittest function before the read statement where the issue occurred. – Michael Behrens Nov 30 '20 at 22:08