Python Pynput: Keyboard and Mouse Listeners and Hotkeys
python pynput keyboard mouse listeners and hotkeys: Learn how to capture global keyboard and mouse input with pynput, define hotkeys, suppress events, and handle platf...
The pynput library exposes global input events to Python programs. Its keyboard.Listener and mouse.Listener classes observe keyboard and mouse activity system-wide, and keyboard.GlobalHotKeys binds callbacks to key combinations. If you need python pynput keyboard mouse listeners and hotkeys for a hotkey launcher, macro recorder, or input monitoring tool, these three classes cover most of what you need.
How the Listener Thread Model Works
A Listener is a thread that runs a platform-specific event loop. When you create a listener, it does not start immediately; you must call start() or use it as a context manager. join() blocks the calling thread until the listener stops.
from pynput import keyboard def on_press(key): print(f"pressed {key}") listener = keyboard.Listener(on_press=on_press) listener.start() # non-blocking # ... other work ... listener.stop()
The context-manager form is more common for short scripts because it guarantees cleanup:
with keyboard.Listener(on_press=on_press) as listener: listener.join()
The listener thread is a daemon thread, so a script that calls start() without join() may exit before any event is observed. join() keeps the process alive while the listener runs.
Capturing Keyboard Events
The keyboard listener accepts on_press and on_release callbacks. Each callback receives a single argument that is either a Key enum member for special keys or a KeyCode instance for character keys.
from pynput import keyboard def on_press(key): try: print(f"character key: {key.char}") except AttributeError: print(f"special key: {key}") def on_release(key): if key == keyboard.Key.esc: return False # stops the listener with keyboard.Listener(on_press=on_press, on_release=on_release) as listener: listener.join()
Returning False from on_press or on_release stops the listener. That is the cleanest way to terminate a keyboard listener from inside its own callback.
The try/except AttributeError pattern is necessary because key.char exists only on KeyCode instances. Special keys such as Key.esc, Key.ctrl, and Key.f5 are Key enum members and have no char attribute.
Capturing Mouse Events
The mouse listener supports on_move, on_click, and on_scroll. Coordinates are absolute screen coordinates in pixels, with the origin at the top-left corner of the primary display.
from pynput import mouse def on_move(x, y): print(f"pointer at ({x}, {y})") def on_click(x, y, button, pressed): if pressed: print(f"{button} pressed at ({x}, {y})") def on_scroll(x, y, dx, dy): print(f"scrolled at ({x}, {y}) by ({dx}, {dy})") with mouse.Listener(on_move=on_move, on_click=on_click, on_scroll=on_scroll) as listener: listener.join()
Returning False from any mouse callback stops the listener. The button argument is a mouse.Button enum member such as Button.left or Button.right. The pressed argument is True on the down event and False on the up event.
Note that on_move fires at the display refresh rate while the pointer is moving, which can be thousands of events per second. Do not put slow work in that callback without queueing.
Defining Global Hotkeys
keyboard.GlobalHotKeys maps hotkey strings to callbacks. The hotkey syntax uses angle-bracket names for modifiers and special keys, joined by +.
from pynput import keyboard def on_activate_h(): print("hotkey <ctrl>+<alt>+h executed") def on_activate_i(): print("hotkey <ctrl>+<alt>+i executed") with keyboard.GlobalHotKeys({ '<ctrl>+<alt>+h': on_activate_h, '<ctrl>+<alt>+i': on_activate_i }) as hotkeys: hotkeys.join()
GlobalHotKeys internally uses HotKey.parse() to convert each string into a sequence of Key and KeyCode objects. The valid modifier names are ctrl, alt, shift, and cmd (on macOS). Special keys use the same names as the Key enum, such as <f5> or <esc>.
If you need finer control over hotkey state, keyboard.HotKey lets you manage the pressed-key set manually. GlobalHotKeys is sufficient for most cases because it handles the state machine internally.
Suppressing Events and Controlling Input
Passing suppress=True to a listener prevents the observed event from reaching the rest of the system. This is how you build a global hotkey that does not also type its own characters.
with keyboard.Listener(on_press=on_press, suppress=True) as listener: listener.join()
Suppression works only for events the listener actually receives. On some platforms, suppressing a key press also suppresses the corresponding release, which can leave modifier keys logically stuck if you are not careful.
For generating input, keyboard.Controller and mouse.Controller send synthetic events. A common pattern is a hotkey callback that presses a different combination:
from pynput.keyboard import Controller, Key controller = Controller() def on_activate_copy(): controller.press(Key.ctrl) controller.press('c') controller.release('c') controller.release(Key.ctrl) with keyboard.GlobalHotKeys({'<ctrl>+<alt>+c': on_activate_copy}) as hotkeys: hotkeys.join()
Controllers are blocking calls. Sending a large burst of synthetic input from a listener callback will stall the listener thread, so queue the work if the burst is large.
Platform Permissions and Failure Modes
Global input access is restricted by the operating system, and the failure modes differ by platform.
On macOS, a process must have Accessibility permission to observe input events and Input Monitoring permission in some macOS versions. Without these, the listener silently receives no events. The user must grant permission in System Settings and restart the process.
On Linux, pynput uses the X11 backend by default. Under a Wayland session, global input listening is generally not available because Wayland does not expose a global input stream to clients. The listener may start but never receive events, or it may raise an error depending on the compositor.
On Windows, the listener generally works without extra configuration, but some keys such as Win may require elevated privileges to suppress.
The practical consequence is that you should test listener behavior on the actual target OS rather than assuming the same event stream across environments. A listener that works on your development machine may be silent in production.
Keeping Listener Callbacks Fast
All listener callbacks run on the listener thread. While a callback is executing, no subsequent event is processed. A slow callback therefore delays every later event, which makes the system feel unresponsive and can cause dropped or delayed input.
The safe pattern is to enqueue work and return quickly:
import queue from pynput import keyboard event_queue = queue.Queue() def on_press(key): event_queue.put(("press", key)) def worker(): while True: event_type, key = event_queue.get() # process event here with keyboard.Listener(on_press=on_press) as listener: listener.start() worker()
The worker runs in the main thread or a separate thread, and the listener thread only performs a queue put, which is fast and non-blocking. This separation matters most for mouse on_move, which can fire hundreds of times per second.