# python-systemd: reading the journal and talking to libsystemd from Python

> python-systemd is the systemd project's own Python binding, split into modules for journal writes and reads, daemon and socket activation helpers, machine and boot identifiers and session queries. The journal module mirrors journalctl behaviour closely enough that you can port a journalctl invocation line for line into Python.

**systemd/python-systemd** — Python wrappers for systemd functionality

- Repository: https://github.com/systemd/python-systemd
- Stars: 524 · Forks: 78
- Language: C
- License: LGPL-2.1
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/systemd-python-systemd

## Four modules, each wrapping a different libsystemd surface

python-systemd is described as a Python module for native access to the systemd facilities, and the README is clear that the functionality is separated into a number of modules rather than dumped into one namespace.

systemd.journal supports sending of structured messages to the journal and reading journal files. systemd.daemon wraps parts of libsystemd useful for writing daemons and socket activation. systemd.id128 provides functions for querying machine and boot identifiers and a list of message identifiers provided by systemd. systemd.login wraps parts of libsystemd used to query logged in users and available seats and machines.

The module boundaries follow the libsystemd API surface, which is the right way to do it. Each of these areas is a separate public API with its own semantics, and lumping them together would have produced a kitchen-sink module. The practical benefit for an adopter is that importing systemd.login does not drag in the journal machinery.

The primary language in the repository metadata is C, and that is accurate: the extension module is C, and the Python side is a thin binding. The pyproject.toml classifies the project for both Programming Language :: C and Programming Language :: Python, with the C classifiers first, and the topic classifiers name Software Development :: Libraries :: Python Modules, System :: Logging and System :: Systems Administration.

The licence is LGPL-2.1, spelled LGPL-2.1-or-later in the pyproject.toml with the SPDX identifier LGPL-2.1-or-later in the source headers. For a Python extension that links against libsystemd, that is the licence you would expect from the systemd project.

The last push was on 2026-06-19 and the recent releases are v236 from 2026-06-18, then v235 and v234 both from 2022-08-16. So the version number tracks systemd's own versioning rather than the module's, and there is a long gap between 2022 and 2026 in the tag history.

## Writing to the journal, and the f-string caveat

The quick example in the README shows the send interface.

```python
from systemd import journal
journal.send('Hello world')
journal.send('Hello, again, world', FIELD2='Greetings!', FIELD3='Guten tag')
journal.send('Binary message', BINARY=b'\xde\xad\xbe\xef')
```

There is one required argument, the message, and additional fields can be specified as keyword arguments. Following the journald API, all names are uppercase.

The sendv call is also accessible directly.

```python
from systemd import journal
journal.sendv('MESSAGE=Hello world')
journal.sendv('MESSAGE=Hello, again, world', 'FIELD2=Greetings!',
               'FIELD3=Guten tag')
journal.sendv('MESSAGE=Binary message', b'BINARY=\xde\xad\xbe\xef')
```

The README says the two examples should give the same results in the log, which is a useful thing to know because the two forms are not obviously equivalent at a glance. sendv is the raw journald interface where you pass the field assignments yourself, and send is the wrapper that builds them for you.

Two notes in the README are worth knowing before you write code against this. Unlike the native C version of journald's sd_journal_send(), printf-style substitution is not supported, and any substitution should be done using Python's f-strings first, or with .format() or the % operator. That is a real difference from the C API and it will bite anyone porting code.

The second is error handling. A ValueError is raised if sd_journald_sendv() results in an error, and this might happen if there are no arguments or one of them is invalid. Since the C call returns an error rather than raising, the binding turns a negative return code into an exception.

A logging handler is provided as well, which is usually the tidiest way to get into the journal from an application.

```python
import logging
from systemd import journal
logger = logging.getLogger('custom_logger_name')
logger.addHandler(journal.JournalHandler(SYSLOG_IDENTIFIER='custom_unit_name'))
logger.warning("Some message: %s", 'detail')
```

That handler routes standard Python logging output into the journal with a SYSLOG_IDENTIFIER, which is how you make a Python daemon's logs look like a native systemd service's logs to journalctl.

## Reading the journal the way journalctl does

The README says reading from the journal is often similar to using the journalctl utility, and then gives a series of examples that map onto journalctl invocations one for one. That is the most useful thing about this module for anyone porting shell scripts to Python.

Showing all entries since 20 minutes ago, which is journalctl --since "20 minutes ago".

```python
from systemd import journal
from datetime import datetime, timedelta
j = journal.Reader()
j.seek_realtime(datetime.now() - timedelta(minutes=20))
for entry in j:
    print(entry['MESSAGE'])
```

Showing entries between two timestamps, which is journalctl --since with --until.

```python
from systemd import journal
from datetime import datetime, timedelta
j = journal.Reader()
since = datetime.now() - timedelta(minutes=50)
until = datetime.now() - timedelta(minutes=10)
j.seek_realtime(since)
for entry in j:
  if entry['__REALTIME_TIMESTAMP'] > until:
    break
  print(entry['MESSAGE'])
```

Note how the upper bound is implemented: the reader seeks to the start position and then breaks out of the loop when it passes the until timestamp, comparing against the __REALTIME_TIMESTAMP field.

Showing explanations of log messages alongside entries, which is journalctl -x.

```python
from systemd import journal
j = journal.Reader()
for entry in j:
    print("MESSAGE: ", entry['MESSAGE'])
    try:
        print("CATALOG: ", j.get_catalog())
    except:
        pass
```

And showing entries by a specific executable, which is journalctl /usr/bin/vim.

```python
from systemd import journal
j = journal.Reader()
j.add_match('_EXE=/usr/bin/vim')
for entry in j:
    print(entry['MESSAGE'])
```

The README notes that matches can be added from many different fields, giving _PID for a specific process ID and _SYSTEMD_UNIT for a specific unit, and links to the systemd.journal-fields documentation for the full set of fields.

Two more cases are covered. Showing the kernel ring buffer is journalctl -k and uses add_match with _TRANSPORT=kernel. And reading entries in reverse, which is journalctl with -r, is done by subclassing the Reader and overriding __next__ to call get_previous and raise StopIteration when there is nothing left.

That reverse-reading example is worth a second look, because it shows the extension point clearly. The class overrides one method and calls a native function, and the rest of the Reader API, seek_tail and add_match, works unchanged underneath.

## Installing it: distro packages first, PyPI second

The README's advice is to use the distribution package wherever possible, and it lists the command for five distributions.

```bash
dnf install python3-systemd
apt update
apt install python3-systemd
zypper in python3-systemd
pacman -Sy python-systemd
```

Those are Fedora, Debian/Ubuntu/Mint, openSUSE and SLE, and Arch respectively. The project is also available on pypi as systemd-python.

That ordering matters more than it might look. The distribution package is built against the distribution's own libsystemd, which means the compatibility rule described below is satisfied by construction, and you get a module that matches the systemd you are running.

The compatibility rule is stated in its own section. This module may be compiled against any version of libsystemd. At compilation time, any functionality that is not available in that version is disabled, and the resulting binary module will depend on symbols that were available at compilation time. The consequence is that the resulting binary module is compatible with that version of libsystemd or any later version, and to obtain maximum possible functionality the module must be compiled against a suitably recent libsystemd.

So the binding never breaks against a newer libsystemd, it just has less in it when built against an older one. That is a deliberate design choice and a good one, but it means a module compiled against an old systemd on an old build host will silently lack functions that exist on the machine you deploy to.

Building from source needs the development headers and a compiler.

```bash
dnf install git python3-pip gcc python3-devel systemd-devel
pip3 install 'git+https://github.com/systemd/python-systemd.git#egg=systemd-python'
```

And on Debian or Ubuntu the package set is different.

```bash
apt install libsystemd-dev gcc python3-dev pkg-config
```

The build system is Meson, with meson-python as the PEP 517 backend and a requires-python of 3.9 or newer. The classifiers cover Python 3.9 through 3.14.

The Makefile wraps the same thing for local development, with build, install, dist and a sign target that gpg-detach-signs the source tarball, plus a doc-sync target that rsyncs the built HTML to www.freedesktop.org. The check target runs pytest over src/systemd/test and the docs directory.

For a quick local test without installing, the README gives the development sequence.

```bash
python -m build
python -m pip install .
python
>>> from systemd import journal
>>> journal.send("Test")
```

## Where python-systemd is the wrong tool

There are three limits, and the first is the tightest one.

The first is that the journal is a systemd concept, not a Linux one. The module links against libsystemd and talks to the journal socket, so there is no journal outside a systemd host. In a container image with a minimal base, or on a host running another init system, systemd.journal has nothing to talk to and the module is either unusable or reduced to the parts that do not need the journal.

The second is that the compatibility rule cuts both ways. Building against an older libsystemd disables functionality rather than failing, which means a module can be installed and imported successfully while quietly missing functions. The README is explicit that maximum functionality requires a suitably recent libsystemd at compile time. For a deployment where you control the build, that is a solvable problem. For a dependency pulled from PyPI as a wheel, you are trusting whoever built it, and the version of systemd on your host may be newer than theirs.

The third is that this is a thin binding, not a higher-level abstraction. There is no structured log format, no trace context propagation, no sampling, no redaction, no metrics. If you want your Python service's logs to be queryable, correlated and queryable in the way a modern platform expects, you are still assembling that yourself on top of journald's field model.

The honest alternatives depend on which of the four modules you want. For sending logs, the standard library's logging module with a StreamHandler to stdout is what a containerised service should use, because the container runtime collects stdout and the platform correlates it. The systemd JournalHandler is for a service running as a native unit, where the journal is where the logs go anyway. For reading logs, journalctl and its JSON output, parsed with a JSON parser in Python, gives you the same data without a C extension and without a libsystemd dependency, at the cost of a subprocess instead of a native reader. For daemon and socket activation, systemd's own protocols are what the module wraps, and there is no real alternative if you want that model, though the sd_notify protocol can be spoken directly over a datagram socket in about ten lines. And for machine and session identifiers, reading the same files under /etc/machine-id and /run/machine-id, or shelling out to systemd-id128, covers most cases.

What python-systemd offers that those do not is being the upstream binding, maintained in the systemd tree, with the semantics matching what the C API does rather than what a Python reimplementation guesses they should be. When you need to interoperate with systemd itself rather than merely log to it, that matters, and the four-module split keeps the surface honest.

## Conclusion

Adopt python-systemd if your Python service needs to write structured journal entries, read the journal with journalctl-like semantics, or use socket activation, since it is the binding the systemd project maintains itself. Do not adopt it for a container image or a non-systemd host, because the module links against libsystemd and the journal is a host concept. Before you install, note that the library is compiled against a libsystemd version and older ones disable functionality, and that the last push was on 2026-06-19 with v236 from 2026-06-18.

## FAQ

### What is python-systemd and what does it include?

It is a Python module for native access to systemd facilities, split into modules: systemd.journal for sending structured messages and reading journal files, systemd.daemon for daemon writing and socket activation, systemd.id128 for machine and boot identifiers and message identifiers, and systemd.login for querying logged in users, seats and machines.

### How do I install python-systemd?

Use the distribution package where possible, with dnf install python3-systemd on Fedora, apt install python3-systemd on Debian, Ubuntu or Mint, zypper in python3-systemd on openSUSE and SLE, and pacman -Sy python-systemd on Arch. It is also on PyPI as systemd-python, and building from source needs the systemd development headers, a compiler and pkg-config.

### How do I read journal entries from Python the way journalctl does?

Use journal.Reader, call seek_realtime with a datetime to start at a point in time, add filters with add_match such as _EXE or _TRANSPORT=kernel, and iterate the entries. The README gives examples for a since window, a since and until window, catalog explanations with get_catalog, the kernel buffer, and reverse reading by subclassing Reader and overriding __next__ to call get_previous.

### Does python-systemd support printf-style substitution when sending messages?

No. Unlike the native C version of journald's sd_journal_send(), printf-style substitution is not supported, and the README says to perform any substitution using Python's f-strings first, or with .format() or the % operator. A ValueError is raised if sd_journald_sendv() results in an error, for example with no arguments or an invalid one.

### What libsystemd version does python-systemd need?

The module may be compiled against any version of libsystemd, and functionality not available in that version is disabled at compilation time, with the resulting binary depending on the symbols available then. It is therefore compatible with that version or any later one, and maximum functionality requires compiling against a suitably recent libsystemd.

## Sources

- [Issues](https://github.com/systemd/python-systemd/issues)
- [License: LGPL-2.1](https://github.com/systemd/python-systemd/blob/main/LICENSE)
- [README](https://github.com/systemd/python-systemd/blob/main/README.md)
- [Releases](https://github.com/systemd/python-systemd/releases)
- [systemd/python-systemd on GitHub](https://github.com/systemd/python-systemd)

---

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