Skip to content

Close the udp socket in close(), not just drop the reference - #3011

Merged
janiversen merged 2 commits into
pymodbus-dev:devfrom
youdie006:udp-close-releases-socket
Aug 27, 2026
Merged

Close the udp socket in close(), not just drop the reference#3011
janiversen merged 2 commits into
pymodbus-dev:devfrom
youdie006:udp-close-releases-socket

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

ModbusUdpClient.close() leaves the socket open

close() documents that it closes the underlying socket connection, but it only clears the
attribute:

# pymodbus/client/udp.py:202
def close(self):
    """Close the underlying socket connection.

    :meta private:
    """
    self.socket = None

Its two siblings both close the socket first — ModbusTcpClient.close() (client/tcp.py:210)
and ModbusSerialClient.close() (client/serial.py:248):

def close(self):
    """Close the underlying socket connection."""
    if self.socket:
        self.socket.close()
    self.socket = None

Reproduction

Two lines of the documented API, on dev at b4ce31d:

import warnings
from pymodbus.client import ModbusUdpClient

warnings.simplefilter("always", ResourceWarning)
client = ModbusUdpClient("127.0.0.1")
client.connect()
sock = client.socket
client.close()
print("connected  :", client.connected)
print("fileno()   :", sock.fileno())      # -1 would mean actually closed

Before:

ResourceWarning: unclosed <socket.socket fd=3, family=2, type=2, proto=0, laddr=('0.0.0.0', 0)>
connected  : False
fileno()   : 3

After:

connected  : False
fileno()   : -1

close() reports success and connected flips to False, so the client looks correctly torn
down while the socket is still open. Under CPython the fd is usually reclaimed by refcounting
once the last reference goes away — which is why this has gone unnoticed — but it is not
reclaimed whenever a reference survives (a retry handler, a traceback frame, a caller holding
client.socket), and CPython itself reports the socket as unclosed on the plain path above.

connect() already routes its bind failure through this same close() at client/udp.py:196,
and #3000 / #3008 just made send()/recv() call self.close() on OSError for the tcp and
serial clients. If udp gets that same treatment, close() needs to actually close.

Change

Two lines in close(), matching tcp and serial. Plus a test in TestSyncClientUdp asserting
the socket is released rather than only dereferenced; it needs no network, since the udp
connect() only creates the socket.

Verified red/green: with the source reverted the test fails assert 16 == -1; with the fix the
file's 35 tests pass. ruff format --check, ruff check and codespell are clean, and pylint
is unchanged at 9.97/10 (the one C0411 on test_client_sync.py:23 is present at dev too).
In a full pytest run the 20 failures are all pre-existing TLS-certificate and live-server
example tests, identical at dev and on this branch.


Disclosure: this fix was written with AI assistance (Claude). I verified the reproduction, the
red/green runs and the test-suite comparison myself.

ModbusUdpClient.close() documents that it closes the underlying socket
connection but only cleared the attribute, so the socket was left open.
ModbusTcpClient.close() and ModbusSerialClient.close() both close it first.

@janiversen janiversen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks

@janiversen
janiversen merged commit c372b23 into pymodbus-dev:dev Aug 27, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants