# ahk is a typed Python wrapper that runs your AutoHotkey scripts in a separate process

> The ahk package is a fully typed Python wrapper around AutoHotkey that exposes mouse, keyboard, window and hotkey control from Python, supporting both AutoHotkey v1 and v2. It fits a Windows developer automating a GUI application, and it does not fit a Linux or macOS environment or a task that should not be driving the mouse.

**spyoungtech/ahk** — Python wrapper for AutoHotkey with full type support. Harness the automation power of AutoHotkey with the beauty of Python.

- Repository: https://github.com/spyoungtech/ahk
- Stars: 1,037 · Forks: 72
- Language: Python
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/spyoungtech-ahk

## The wrapper is a bridge to an AutoHotkey process, and that shapes everything

The ahk package is a fully typed Python wrapper around AutoHotkey, and its description is that it harnesses the automation power of AutoHotkey with the beauty of Python.

The architecture is worth understanding before anything else, because it explains the API shape. The Python side is a client, and an AutoHotkey process does the actual work. Every call is a round trip to that process.

The consequence shows up in the signature of the very first example:

```python
from ahk import AHK

ahk = AHK()

ahk.mouse_move(x=100, y=100, blocking=True)  # Blocks until mouse finishes moving (the default)
ahk.mouse_move(x=150, y=150, speed=10, blocking=True) # Moves the mouse to x, y taking 'speed' seconds to move
print(ahk.mouse_position)  #  (150, 150)
```

The blocking parameter exists because the underlying call is asynchronous on the AutoHotkey side and the Python wrapper has to wait for the AHK process to report that the mouse finished moving. A caller who does not want to wait passes blocking=False, and a caller who wants the mouse to glide over a second passes a speed.

So the cost model is clear. A single keystroke or mouse move costs an inter-process round trip, and a script that performs a thousand of them will spend its time waiting on the bridge rather than doing work. For GUI automation where each action involves a human-speed delay anyway, that is irrelevant. For a tight loop, it is not.

## Hotkeys and hotstrings are the part that runs asynchronously

Hotkeys can be configured to run Python functions as callbacks, and the example is the clearest demonstration of the package's value:

```python
from ahk import AHK

def my_callback():
    print('Hello callback!')

ahk = AHK()
# when WIN + n is pressed, fire `my_callback`
ahk.add_hotkey('#n', callback=my_callback)
ahk.start_hotkeys()  # start the hotkey process thread
ahk.block_forever()  # not strictly needed in all scripts -- stops the script from exiting; sleep forever
```

Now whenever Windows key plus n is pressed, the callback function is called in a background thread. That is the thing a pure AutoHotkey script cannot do, since AHK has no way to call into Python, and it is why the wrapper starts a separate process for hotkeys rather than handling them inline.

The lifecycle rules are documented as notes, and they matter. Hotkeys run in a separate process that must be started manually with ahk.start_hotkeys(). They can be stopped with ahk.stop_hotkeys(), which will not stop actively running callbacks. Hotstrings share the same process with hotkeys and are started and stopped in the same manner. And if hotkeys or hotstrings are added or removed while the process is running, the underlying AHK process is restarted automatically.

That last rule is a subtle design consequence. Modifying the hotkey set at runtime restarts the process, so code that registers a hotkey inside a loop pays a process restart each time.

Exception handling is per hotkey, so a failing callback does not take down the process:

```python
ahk.add_hotkey('#n', callback=go_boom, ex_handler=my_ex_handler)
```

Hotstrings work the same way and can be plain string replacements or Python callbacks:

```python
ahk.add_hotstring('btw', 'by the way')  # string replacements
ahk.add_hotstring('btw', my_callback) # call python function in response to the hotstring
```

And they can be removed individually or cleared wholesale with remove_hotstring and clear_hotstrings.

## Mouse and keyboard calls are thin wrappers over AHK commands

The mouse and keyboard sections show a consistent style: each Python method maps onto an AutoHotkey command, with the coordinate mode made explicit where it matters.

For the mouse, the coordinate mode is the detail to understand. ahk.mouse_position returns a tuple of mouse coordinates relative to the active window, and get_mouse_position with coord_mode='Screen' gives coordinates relative to the screen. Movement is absolute through the setter, and click takes an optional button, a click count and a coordinate mode, so ahk.click(button='R', click_count=2) is a right double-click, and mouse_drag holds the primary button while moving.

For the keyboard, the distinction is between type and send_input. ahk.type('hello, world!') sends keys as if typed and performs string escapes for you. ahk.send_input('Hello, {U+1F30E}{!}') is like AHK SendInput, but unlike type, control sequences must be escaped manually, and the characters !^+#= and the braces must be escaped by hand.

That difference is the kind of thing that causes a bug report, because the two methods look interchangeable until a message contains an exclamation mark.

The state methods are the other half. key_state('Control') reports whether a key is down, and key_state('CapsLock', mode='T') checks a toggle state, for things like NumLock and CapsLock. key_press, key_down and key_up give explicit press and release control, set_capslock_state turns the toggle on, and key_wait waits for a key with a timeout and returns a boolean.

The typed surface is the point of the package. Every method has a signature, so an editor can complete it and a type checker can check the call, which is the difference the package description is making when it says fully typed.

## Windows can be found, waited for and driven without focus

The window API is where the wrapper earns its keep for real automation, because it does not depend on the window having focus.

Windows can be obtained in several ways. ahk.active_window gets the active one, win_get takes a title, list_windows returns all of them, win_get_from_mouse_position finds the one under the cursor, and win_get can also take a pid as a title, since the example uses title='ahk_pid 20366'.

Waiting is a first-class operation rather than a polling loop you write yourself. win_wait takes a title and a timeout, and raises TimeoutError when the window does not appear, with win_wait_active and win_wait_not_active available as the active-window variants. The example catches TimeoutError and reports that Notepad was not found.

The working-with-windows section shows the capability that matters. win.send('hello', control='Edit1') sends keys directly to the window and does not need focus, with control_send offered as the module-level alternative. The window object also has move with x, y, width and height, and activate.

The absence of focus is the design point. A script that types into a window it has just found will otherwise steal the caret from whatever the user is doing, and a test suite that runs against several applications at once will fight itself. Addressing controls directly sidesteps that.

The repository layout backs the API surface. The ahk/ directory holds the package, docs/ the documentation built on Read the Docs, tests/ the test suite, and the build tooling includes buildunasync.py and a .unasync-rewrite.py, which indicates that the synchronous API is generated from an async one rather than written twice.

## The build is asyncio-first, with a generated sync surface

The repository layout gives away an unusual and interesting design decision. Alongside the ahk/ package, docs/ and tests/, there is buildunasync.py, a .unasync-rewrite.py file, a .pre-commit-config.yaml, a tox.ini, a requirements-dev.txt, and a _set_constants.py.

The unasync tool takes an asynchronous Python source file and rewrites it into a synchronous one, replacing await with a synchronous call and applying other transformations. That is why the build requires unasync from a GitHub archive URL in the build-system requires, alongside tokenize-rt, which tokenizes the source so the rewrite can locate the async constructs.

The consequence for a user is that the synchronous API is generated, and the async one is the source of truth. The topic list on the repository includes asyncio, and the package documentation describes an async API alongside the blocking one.

That is a better answer to the inter-process round trip problem than a purely synchronous wrapper would be, because the expensive part of the API, waiting for the AHK process to report that a mouse move finished or a window appeared, can be awaited instead of blocking a thread.

The build backend itself is plain setuptools, with setup.py reduced to a single setup() call and everything else in setup.cfg and pyproject.toml. The package is published to PyPI as ahk, and the versioning is managed by the _set_constants.py script. The CI runs a test workflow and there is a Coveralls badge, so the test suite is measured.

## The runtime dependency AutoHotkey itself is the part to plan for

Installation is a single command, and the requirement list has a line that matters more than it looks:

```
pip install ahk
```

Python 3.8 and later is required, and the package supports AutoHotkey v1 and v2. The README points to a section on non-Python dependencies for the details of what else has to be present on the machine.

That is the structural constraint of the whole design. The Python package is only a client, so AutoHotkey has to be installed and working on the target machine, and a deployment that installs ahk via pip and assumes it is done is wrong. A container image that wants to run this needs the AHK runtime baked in, and a machine that upgrades AutoHotkey under the wrapper may change behaviour, because v1 and v2 differ in their command syntax and the wrapper supports both.

There is also a platform constraint that follows from the same place. AutoHotkey is a Windows automation tool, so this package is a Windows tool, and the repository topics list windows alongside python, asyncio, autohotkey and automation.

The release history shows steady maintenance. The versions are v1.8.4 from 2025-04-02, v1.8.3 from 2025-02-01 and v1.8.2 from 2025-01-15, and the last push was on 2026-10-13, so the work is ongoing.

## Where the ahk package is the wrong tool

The first wrong-tool case is anything that is not Windows. AutoHotkey is a Windows tool, and this wrapper does nothing without it. A team on Linux or macOS has no path here at all, and a team that needs automation on both platforms needs two different solutions.

The second is a task that should not be driving the mouse. Any automation built on synthetic input is fragile in a way that API-based automation is not. A window that reflows, a control that moves, a different theme, a focus change, and the script clicks the wrong thing. A tool that talks to an application's UI Automation interface or its API is more robust, and this package does not attempt to be that.

The third is throughput. Every call is an inter-process round trip to an AHK instance, and a loop of a thousand mouse moves spends its time in the bridge. The library is built for the kind of automation where a human-speed delay is already in the path, and a performance-sensitive loop should be written in AutoHotkey directly rather than driven from Python.

The fourth is a reliability expectation. A script that types into a dialog and clicks OK is fine for a personal tool, and wrong for a production pipeline where a stray focus change means a wrong click. The hotkey callback model has the same property from the other side: the callback runs in a background thread, and a background thread that blocks holds up the hotkey process. The project is MIT licensed, so the licence is not a constraint, and the repository carries a pre-commit configuration and tox.ini, which means the test story is reasonably well kept.

## Conclusion

ahk fits a Windows developer who needs to automate a desktop application from Python, whether that means sending keystrokes, driving the mouse, or reacting to a hotkey in a background thread. It does not fit a cross-platform team or a task where the output has to be reliable enough to ship. Before adopting it, install the AutoHotkey runtime itself, since the wrapper is only a bridge, and note that every call crosses a process boundary to an AHK instance, so a script that hammers the API in a loop will be slower than the equivalent AHK script.

## FAQ

### How do I install and use the ahk Python package?

Run pip install ahk, which requires Python 3.8 or later and supports AutoHotkey v1 and v2. Create an AHK() instance and call methods on it, for example ahk.mouse_move(x=100, y=100, blocking=True), where the default blocking behaviour waits until the mouse finishes moving.

### How do hotkeys call Python functions with the ahk package?

Register the callback with add_hotkey, for example ahk.add_hotkey('#n', callback=my_callback), then call ahk.start_hotkeys() to start the hotkey process thread. Hotkeys run in a separate process that must be started manually, and adding or removing one while it runs restarts the underlying AHK process automatically.

### What is the difference between type and send_input in the ahk package?

ahk.type sends keys as if typed and performs string escapes for you, while ahk.send_input behaves like AHK SendInput and requires control sequences to be escaped manually, including the characters !^+#= and the braces. The two look interchangeable until a message contains one of those characters.

### Can the ahk package control a window without giving it focus?

Yes. The Window object has a send method that sends keys directly to the window and does not need focus, and there is a module-level control_send alternative. The package also offers win_wait with a timeout, which raises TimeoutError when the window is not found.

## Sources

- [Issues](https://github.com/spyoungtech/ahk/issues)
- [License: MIT](https://github.com/spyoungtech/ahk/blob/main/LICENSE)
- [README](https://github.com/spyoungtech/ahk/blob/main/README.md)
- [Releases](https://github.com/spyoungtech/ahk/releases)
- [spyoungtech/ahk on GitHub](https://github.com/spyoungtech/ahk)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spyoungtech-ahk
