Code for How to Control your Keyboard in Python Tutorial


View on Github

keyboard_controller_2026.py

from __future__ import annotations

import argparse
import platform
import sys
import time
from dataclasses import dataclass
from typing import Callable


@dataclass(frozen=True)
class Action:
    """A keyboard action that can be previewed or executed."""

    description: str
    callback: Callable[[object], None]


def import_keyboard():
    """Import keyboard lazily so --dry-run works in headless/test environments."""
    try:
        import keyboard
    except ImportError as exc:
        raise SystemExit("Install the dependency first: pip install keyboard") from exc
    return keyboard


def platform_hint() -> str:
    """Return a short note about common platform requirements."""
    system = platform.system().lower()
    if system == "linux":
        return "Linux usually needs root privileges or access to /dev/input for global hooks."
    if system == "darwin":
        return "macOS support in the keyboard package is limited; pynput or PyAutoGUI is often a better fit."
    if system == "windows":
        return "Windows is the smoothest platform for the keyboard package."
    return "Global keyboard hooks depend on your operating system and permissions."


def preview_or_run(action: Action, keyboard_module: object | None, execute: bool) -> None:
    """Print what would happen, or run the action when --execute is set."""
    if not execute:
        print(f"DRY RUN: {action.description}")
        return
    if keyboard_module is None:
        raise RuntimeError("keyboard module is required when execute=True")
    print(f"RUNNING: {action.description}")
    action.callback(keyboard_module)


def demo_typing(execute: bool) -> None:
    """Type text into the currently focused window."""
    keyboard = import_keyboard() if execute else None
    actions = [
        Action(
            "type 'Hello from Python!' with a small delay",
            lambda kb: kb.write("Hello from Python!", delay=0.03),
        ),
        Action("press Enter", lambda kb: kb.press_and_release("enter")),
        Action("send Ctrl+A", lambda kb: kb.send("ctrl+a")),
    ]
    for action in actions:
        preview_or_run(action, keyboard, execute)


def demo_hotkeys(execute: bool) -> None:
    """Register a global hotkey and an abbreviation."""
    keyboard = import_keyboard() if execute else None

    if not execute:
        print("DRY RUN: register Ctrl+Alt+H to print a message")
        print("DRY RUN: expand '@email' into 'name@example.com'")
        print("DRY RUN: wait until Esc is pressed")
        return

    keyboard.add_hotkey("ctrl+alt+h", lambda: print("Ctrl+Alt+H pressed"))
    keyboard.add_abbreviation("@email", "name@example.com")
    print("Hotkeys are active. Type @email + space in another app, or press Ctrl+Alt+H.")
    print("Press Esc to stop.")
    keyboard.wait("esc")
    keyboard.unhook_all()


def demo_record(execute: bool) -> None:
    """Record keyboard events until Esc, then replay them after confirmation."""
    keyboard = import_keyboard() if execute else None

    if not execute:
        print("DRY RUN: record keyboard events until Esc")
        print("DRY RUN: show typed strings")
        print("DRY RUN: ask before replaying events")
        return

    print("Recording now. Press Esc to stop.")
    events = keyboard.record("esc")
    typed = list(keyboard.get_typed_strings(events))
    print("Typed strings:", typed)

    answer = input("Replay the recorded events? [y/N] ").strip().lower()
    if answer == "y":
        print("Replaying in 3 seconds. Focus the target window now.")
        time.sleep(3)
        keyboard.play(events)
    keyboard.unhook_all()


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Safe examples for controlling the keyboard in Python.")
    parser.add_argument(
        "demo",
        choices=("typing", "hotkeys", "record"),
        help="Which demo to run",
    )
    parser.add_argument(
        "--execute",
        action="store_true",
        help="Actually send/listen to keyboard events. Without this flag, the script only prints what it would do.",
    )
    args = parser.parse_args(argv)

    print(platform_hint())
    if not args.execute:
        print("Running in dry-run mode. Add --execute on your own desktop to perform the actions.\n")

    if args.demo == "typing":
        demo_typing(args.execute)
    elif args.demo == "hotkeys":
        demo_hotkeys(args.execute)
    elif args.demo == "record":
        demo_record(args.execute)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())