# libtmux: a typed Python object model over tmux, with an escape hatch

> libtmux replaces shelling out to tmux and parsing its output with typed Python objects for servers, sessions, windows and panes, plus filtering, traversal, self-location, a pytest plugin and a raw `.cmd()` escape hatch. This looks at the API shape, the version policy, and what the wrapper does not hide.

**tmux-python/libtmux** — ⚙️ Python API / wrapper for tmux

- Repository: https://github.com/tmux-python/libtmux
- Website: https://libtmux.git-pull.com
- Stars: 1,216 · Forks: 126
- Language: Python
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/tmux-python-libtmux

## Stop shelling out and parsing tmux ls

The pitch in one line is in the readme: stop shelling out and parsing `tmux ls`, and instead interact with real Python objects, `Server`, `Session`, `Window` and `Pane`.

That is the whole problem the library solves. Driving tmux from a script normally means running `tmux` commands and parsing their output, which is a text format that changes between versions, and it means a failure is a non-zero exit code rather than an exception. libtmux turns that into an object model where a session is a `Session` with a `session_id` and a `session_name`, and a lookup that does not find what you asked for fails in the way a Python programmer expects.

The project describes itself as a typed library that provides an ORM wrapper for tmux, and the ORM comparison is apt: it is a mapping between a stateful external system and Python objects with relationships between them, where the relationships are navigable in both directions.

The feature list adds the parts that make a wrapper usable in anger. You can query and traverse live sessions, windows and panes. You can locate yourself from inside a pane with `from_env()`. Every object has a `.cmd(...)` raw escape hatch. It works with multiple tmux sockets and servers. There are context managers for automatic cleanup, and a pytest plugin for isolated tmux fixtures. And the same API powers tmuxp, which is the argument that it is battle-tested in real workflows rather than a weekend project.

## The object model, and how the relationships navigate

The core concepts section maps each libtmux object to its tmux counterpart, and the traversal API is what makes the model more than a naming convention.

Navigation goes up the hierarchy, so a pane knows its window and its session:

```python
>>> pane.window
Window(@... ...:..., Session($... ...))
>>> pane.window.session
Session($... ...)
```

Windows are created and renamed as objects, and the collections on a session behave like a queryable set rather than a list of strings:

```python
>>> session.rename_session('my-session')
Session($... my-session)

>>> bg_window = session.new_window(attach=False, window_name="bg-work")
>>> session.windows.filter(window_name__startswith="bg")
[Window(@... ...:bg-work, Session($... ...))]

>>> session.windows.get(window_name__startswith="bg")
Window(@... ...:bg-work, Session($... ...))
```

`filter()` and `get()` are the two operations worth noticing. `filter()` takes keyword arguments that map onto tmux attributes, with a `__` suffix for operators such as `startswith`, and `get()` is the direct lookup, by `session_id` for sessions or by attribute for windows.

Panes are manipulated the same way, split without switching to them, sent keystrokes to, and read back:

```python
>>> pane = window.split(attach=False)
>>> pane.send_keys('echo hello')
>>> pane.send_keys('echo hey', enter=False)
>>> pane.enter()
```

And the output is captured through the raw command path:

```python
>>> pane.cmd('capture-pane', '-p').stdout
["$ echo 'hello world'", 'hello world', '$']
```

That last example is the design in miniature. The convenient API and the escape hatch are the same object, and the documentation is comfortable showing both.

## from_env(), and knowing which pane you are

The feature the readme spends most words on is self-location, and it is the one that has no equivalent in shell scripting.

tmux writes `TMUX` and `TMUX_PANE` into every pane it spawns, and `Server`, `Session`, `Window` and `Pane` each read them back. The readme's framing is that code running inside a pane, a script in a split, a tmux hook or an agent, can ask where it is.

```python
>>> Pane.from_env()
Pane(%... ...)
>>> Session.from_env().session_name == session.session_name
True
```

Outside tmux there is no pane to return, and `from_env()` raises `NotInsideTmux`. The readme also explains the shape of its own example, noting that in a real pane tmux has already set those two variables so `from_env()` takes no arguments, and that the README is not running in a pane, which is why the example sets them first.

That is a small but real design decision. The alternative is to make self-location best effort and return nothing when the variables are absent, and then every caller has to handle a None that in practice means either an error or a bug. Raising a specific exception keeps the failure loud.

For a tool that runs as a tmux hook, or a script that a user runs inside a split, that turns the environment into a reliable way to address the pane it is running in, which is what makes hooks and agents practical to build on top of tmux at all.

## Installation, and the advice to pin a range

The install section offers the usual Python tooling, and the useful detail is at the end of it.

```console
$ pip install libtmux
```

```console
$ pipx install libtmux
```

```console
$ uv add libtmux
```

```console
$ uvx --from "libtmux" python
```

And for the bleeding edge version from the main branch:

```console
$ pip install 'git+https://github.com/tmux-python/libtmux.git'
```

Then the tip, which is the part to copy into your own project. libtmux is pre-1.0, so pin a range in projects to avoid surprises:

```ini
libtmux==0.50.*
```

```toml
libtmux = "0.50.*"
```

An exact pin of a 0.x version is not a sensible policy for a library that is still moving, and a range pin is what the maintainers themselves ask for. The `uvx --from "libtmux" python` line is a nicety, since it gives you a one-off interpreter with the library installed, which is the quickest way to try the API in a REPL before it touches your project.

For a first look, the readme suggests using ptpython or ipython for a REPL with autocompletions, and points out that tmuxp's own `tmuxp shell` drops you straight into your current server, session, window and pane.

## The tmux version floor, and the branches that stay behind

The requirements are short and have consequences. tmux must be 3.2a or later, and Python must be 3.10 or later, on CPython and PyPy.

Below that, the project keeps maintenance-only branches, and it is careful about the word maintenance. For Python 2.x there is the v0.8.x branch, and for tmux 1.8 through 3.1c there is v0.48.x. Both are described as maintenance-only backports, with no new fixes.

So a team on an old tmux can still use libtmux, but they are pinning themselves to a branch that receives nothing but backports, and the recent release titles show why that matters. v0.60.0 is tmux 3.7 parity, v0.61.0 is tmux 3.7a and 3.7b test fixes, and v0.62.0 is locate yourself and resolve linked windows. Each of those is a change that only makes sense against a recent tmux, because the features and the bugs are in tmux itself.

The classification in the manifest matches that: Development Status 5, Production/Stable, Typed, Framework :: Pytest, with the operating systems listed as POSIX and MacOS X. So the project considers itself stable, and the version floor is part of what that means. A Linux and macOS team on a current tmux is the supported case, and a Windows team is out of scope by the classifiers rather than by an explicit prohibition.

The current version is 0.62.0, and the manifest requires Python `>=3.10,<4.0` with classifiers for Python 3.10 through 3.14.

## Tests and docs are part of the package

The repository layout shows a project that treats its test suite and documentation as deliverables rather than as chores.

There is a `justfile` with grouped recipes. The test recipe is `uv run py.test`, there is a `start` recipe that runs the tests and then `ptw` for continuous testing with pytest-watcher, and a `watch-test` recipe that pipes the project's file list through `entr` and falls back with a warning when `entr` is not installed. The documentation has its own recipes, `build-docs`, `watch-docs`, `serve-docs`, `dev-docs` and `start-docs`, with a separate `docs/justfile`.

The pytest plugin is the feature that matters most for a library like this, because testing code that talks to tmux means running a real tmux server, and the usual failure mode is a test suite that leaves servers running and eventually exhausts the machine. The plugin provides isolated tmux fixtures, and the context managers give the same guarantee in application code.

The manifest backs it up with a `Framework :: Pytest` classifier and a `Typing :: Typed` one, and the `pyproject.toml` ships the `tests`, `docs`, `conftest.py`, `CHANGES`, `MIGRATION` and the project's own `.tmuxp.yaml` in the sdist, which is the pattern of a project that expects people to read its tests.

There is also a `CITATION.cff` and an `AGENTS.md` plus a `CLAUDE.md`, and the repository root carries a `TODO` file, a `CHANGES` file and a `MIGRATION` file, the last being the one to read before a major version bump.

## Conclusion

libtmux fits a Python tool that has to create, inspect and drive tmux sessions, and a test suite that needs isolated tmux fixtures, because the pytest plugin and the context managers solve the cleanup problem that shell scripting leaves to the caller. It does not fit a project that does not already assume tmux, and it is the wrong choice for a Windows-only deployment given the POSIX and macOS classifiers. Before you depend on it, pin a version range as the readme advises, since the project is still pre-1.0, and check that your tmux is 3.2a or newer, because older tmux versions are supported only by the maintenance-only v0.48.x branch.

## FAQ

### What does libtmux do?

It provides typed, object-oriented control over tmux. Instead of running tmux commands and parsing their output, you work with Server, Session, Window and Pane objects, and the same API powers tmuxp.

### How do I install libtmux?

With `pip install libtmux`, `pipx install libtmux`, `uv add libtmux` or `uvx --from "libtmux" python`, and from the main branch with `pip install 'git+https://github.com/tmux-python/libtmux.git'`.

### What tmux and Python versions does libtmux need?

tmux 3.2a or later and Python 3.10 or later on CPython or PyPy. Older tmux versions from 1.8 to 3.1c and Python 2.x are supported by the maintenance-only v0.48.x and v0.8.x branches, which receive no new fixes.

### How do I find the pane my code is running in?

Call `Pane.from_env()` or `Session.from_env()`. tmux writes TMUX and TMUX_PANE into every pane it spawns, and the objects read them back. Outside tmux there is no pane to return and `from_env()` raises NotInsideTmux.

### Should I pin a specific libtmux version?

The readme advises pinning a range rather than an exact version, because libtmux is pre-1.0, and gives `libtmux==0.50.*` for requirements.txt and `libtmux = "0.50.*"` for pyproject.toml as the examples.

## Sources

- [License: MIT](https://github.com/tmux-python/libtmux/blob/master/LICENSE)
- [Project website](https://libtmux.git-pull.com)
- [README](https://github.com/tmux-python/libtmux/blob/master/README.md)
- [Releases](https://github.com/tmux-python/libtmux/releases)
- [tmux-python/libtmux on GitHub](https://github.com/tmux-python/libtmux)

---

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