How to rotate UV and preserve the correct aspect ratio?
Clearly the UV editor uses the aspect ratio.

Closing the image in the UV editor on LHS demonstrates that the UV editor is using the aspect ratio of the image when rotating.  The LHS rotation result is equivalent to running script result in question, or with unit aspect ratio below.
Instead of our normal 2D rotation matrix,
$$R=\left[
\begin{array}{ccc}
 \cos \theta &amp; \sin \theta&amp; 0 \\
 -\sin\theta &amp; \cos\theta &amp; 0 \\
 0 &amp; 0 &amp; 1 \\
\end{array}
\right]$$
can allow for a non uniform aspect ratio $\rho$
$$R=\left[
\begin{array}{ccc}
 \cos \theta &amp; \dfrac{\sin \theta}{\rho } &amp; 0 \\
 -\rho  \sin\theta &amp; \cos \theta&amp; 0 \\
 0 &amp; 0 &amp; 1 \\
\end{array}
\right]$$
Here is a an object mode script using numpy and the $XY$ dimension of the object (for example, if imported using image as planes) to calculate $\rho$ which will emulate the rotation as shown on RHS of gif.  Setting p = 1 will be same as shown on LHS.
import bpy
from mathutils import Matrix, Vector
import numpy as np

ob = bpy.context.object
me = ob.data
uvlayer = me.uv_layers.active

pivot = Vector((0.5, 0.5))
angle = np.radians(30)
# aspect ratio
p = ob.dimensions.y / ob.dimensions.x
# aspect rotate
R = Matrix((
            (np.cos(angle), np.sin(angle) / p),
            (-p * np.sin(angle), np.cos(angle)),
        ))
    
uvs = np.empty(2 * len(me.loops))
uvlayer.data.foreach_get(&quot;uv&quot;, uvs)

# shear rotate about origin, translate to pivot
uvs = np.dot(
        uvs.reshape((-1, 2)) - pivot, 
        R) + pivot
# write the new UV's back
uvlayer.data.foreach_set(&quot;uv&quot;, uvs.ravel())
# update mesh to display changes.
me.update()


Result on image imported from &quot;Images as Planes&quot; addon. Note positive rotation is clockwise, where-as blender uses CCW (simply change sign of angle to change)