Can't tie custom operator to a button (invalid operator call '__OT_button')

Hi! I’ve recently started learning Python and Blender scripting. For practice, I’ve made a super simple function that checks if my selection is empty or not and prints the result in the console, and I’ve been trying to create a simple UI panel with a button for it. The function runs fine if I run it directly, but trying to make a custom operator out of it and give it a simple UI gives me an error that I haven’t been able to figure out.

My code:

import bpy

class SV_EMPTYSELECTION_OP(bpy.types.Operator):
    bl_idname = "emptyselection.button"
    bl_label = "Check for Empty Selection"
    bl_description = "Check if selection is empty or not"
    bl_options = {'REGISTER', 'UNDO'}

    selected_objects = bpy.context.selected_objects

    def empty_selection():
        if (len(selected_objects)) == 0:
            print("Empty selection")
        else:
            print("Not empty selection")

class SV_PT_EMPTYSELECTION_PANEL(bpy.types.Panel):
    bl_label = "Empty Selection Checker"
    bl_idname = "SV_PT_EMPTYSELECTION_PANEL"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Empty Selection Checker"

    def draw(self, context):
        layout = self.layout
        row = layout.row(align=True)
        row.operator(SV_EMPTYSELECTION_OP.bl_idname)
        
    def execute(self, context):
        main(context, True, self.when_true)
        return {'FINISHED'}
        
def register():
    bpy.utils.register_class(SV_PT_EMPTYSELECTION_PANEL)
    bpy.utils.register_class(SV_EMPTYSELECTION_OP)

def unregister():
    bpy.utils.unregister_class(SV_PT_EMPTYSELECTION_PANEL)
    bpy.utils.unregister_class(SV_EMPTYSELECTION_OP)

if __name__ == "__main__":
    register()

Running this script creates a panel in the N menu of the 3D viewport with a button to execute my custom operator, as expected. Clicking it gives me this console error:

ERROR (wm.operator): C:\Users\blender\git\blender-v360\blender.git\source\blender\windowmanager\intern\wm_event_system.cc:1551 wm_operator_invoke: invalid operator call 'EMPTYSELECTION_OT_button'

I know this has to do with this line

    bl_idname = "emptyselection.button"

as renaming “emptyselection” to something else changes what comes before _OT_button. That’s about as far as I’ve been able to get in debugging.

What’s confusing is that I’ve followed the overall structure of another addon I use that does the same thing: creates a custom operator and a simple UI with a button that runs this operator, and that addon works just fine. I can’t figure out what the difference is that makes my script not work. Can anyone tell what’s wrong with this?

import bpy

class EMPTYSELECTION_OT_button(bpy.types.Operator):
    bl_idname = "emptyselection.button"
    bl_label = "Check for Empty Selection"
    bl_description = "Check if selection is empty or not"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        if (len(context.selected_objects)) == 0:
            print("Empty selection")
        else:
            print("Not empty selection")
        return {'FINISHED'}

class SV_PT_EMPTYSELECTION_PANEL(bpy.types.Panel):
    bl_label = "Empty Selection Checker"
    bl_idname = "SV_PT_EMPTYSELECTION_PANEL"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Empty Selection Checker"

    def draw(self, context):
        layout = self.layout
        row = layout.row(align=True)
        row.operator(EMPTYSELECTION_OT_button.bl_idname)
        
def register():
    bpy.utils.register_class(SV_PT_EMPTYSELECTION_PANEL)
    bpy.utils.register_class(EMPTYSELECTION_OT_button)

def unregister():
    bpy.utils.unregister_class(SV_PT_EMPTYSELECTION_PANEL)
    bpy.utils.unregister_class(EMPTYSELECTION_OT_button)

if __name__ == "__main__":
    register()

there is a a bit “hidden” operator tutorial in the reference: https://docs.blender.org/api/current/bpy.types.Operator.html. make sure you study it too, there are a lot of such tutorials scattered around in the docs