Tutorial - First Steps¶
Let's build your first Python script that manages systemd services. We'll go step by step, from listing services to following live logs.
Prerequisites
Make sure you've completed the Installation guide and
that systemd-client is working on your system.
1. Create a client¶
Create a file called main.py and add:
- Import the synchronous client. That's the only import you need to get started.
- Create a client instance. By default it uses the subprocess backend, which
calls
systemctl --userandjournalctl --userunder the hood -- no extra dependencies required.
Tip
You can also use SystemdClient as a context manager:
scope=SystemdScope.SYSTEM.
2. List your services¶
Now let's list all running user services. Add this to your script:
from systemd_client import SystemdClient
client = SystemdClient()
for unit in client.list_units(unit_type="service"): # (1)!
print(f"{unit.name}: {unit.active_state} ({unit.sub_state})") # (2)!
list_units()returns a list ofUnitInfoobjects. Theunit_typefilter narrows it to services only (you could also use"timer","socket", etc.).- Each
UnitInfohas typed attributes likename,active_state,sub_state,load_state, anddescription-- no string parsing needed.
Run it:
You should see
The exact output depends on what user services you have running.Filtering by state
You can also filter by state to find, say, only failed services:
3. Check unit status¶
Let's get detailed information about a specific service. Replace the content of
main.py with:
from systemd_client import SystemdClient
client = SystemdClient()
status = client.status("dbus.service") # (1)!
print(f"Name: {status.name}")
print(f"State: {status.active_state} ({status.sub_state})") # (2)!
print(f"PID: {status.main_pid}")
print(f"Since: {status.active_enter_timestamp}") # (3)!
status()returns aUnitStatusobject with detailed properties. Pass the full unit name including the suffix.active_stateis the high-level state ("active","inactive","failed").sub_stategives more detail ("running","dead","exited").active_enter_timestampis adatetimeobject -- you can format it, compare it, or do math with it.
Note
If you get a UnitNotFoundError, the unit doesn't exist. Always use the full
unit name with its suffix (e.g., my-app.service, not just my-app).
4. Start and stop a service¶
Now let's manage a service. The client has start(), stop(), restart(), and
reload() methods:
from systemd_client import SystemdClient
client = SystemdClient()
client.stop("my-app.service") # (1)!
print(f"Active: {client.is_active('my-app.service')}") # (2)!
client.start("my-app.service") # (3)!
print(f"Active: {client.is_active('my-app.service')}")
status = client.status("my-app.service")
print(f"PID: {status.main_pid}")
stop()sends the stop command and waits for systemd to acknowledge it.is_active()is a quick boolean check -- much cheaper than callingstatus()when you only need to know if it's running.start()brings the service back up.
Warning
Replace my-app.service with an actual user service you have. If you don't
have one, dbus.service is always available -- but be careful stopping
system-critical services.
Enable and disable
To control whether a service starts at boot, use enable() and disable():
result = client.enable("my-app.service") # (1)!
for change in result.changes:
print(f" {change[0]} {change[1]} -> {change[2]}")
client.disable("my-app.service")
enable()returns anEnableResultwith a list of symlink changes that were made.
5. Query the journal¶
One of the most powerful features: reading service logs with structured data. Let's query the last 20 log entries for a service:
from systemd_client import SystemdClient, JournalPriority # (1)!
client = SystemdClient()
entries = client.journal("my-app.service", lines=20) # (2)!
for entry in entries:
print(f"[{entry.priority.name}] {entry.message}") # (3)!
errors = client.journal( # (4)!
"my-app.service",
priority=JournalPriority.ERR,
since="1h ago",
)
print(f"\nErrors in last hour: {len(errors)}")
- Import
JournalPriorityto filter by log level. journal()returns a list ofJournalEntryobjects. Each one has typed fields:message,priority,timestamp,pid, and more.priorityis aJournalPriorityenum with levels fromEMERG(0) toDEBUG(7).- Combine filters: get only errors (
priority <= ERR) from the last hour. Thesinceparameter accepts human-friendly strings like"1h ago","today", or ISO dates like"2026-03-31".
You should see
More journal filters
The journal() method supports several filters you can combine:
entries = client.journal(
"my-app.service",
lines=100, # last N entries
since="2h ago", # start time
until="30m ago", # end time
priority=JournalPriority.WARNING, # WARNING and above
grep="timeout|error", # regex filter on message text
)
See the Journal Guide for the full set of options.
6. Follow the journal in real-time¶
You can tail the journal live, like journalctl -f. This creates a blocking
iterator that yields new entries as they arrive:
from systemd_client import SystemdClient
client = SystemdClient()
for entry in client.journal_follow("my-app.service"): # (1)!
ts = entry.timestamp.strftime("%H:%M:%S") if entry.timestamp else ""
print(f"{ts} [{entry.priority.name}] {entry.message}")
journal_follow()returns an iterator that blocks until a new log line arrives. Press ++ctrl+c++ to stop.
You should see
New log lines appearing in real-time as the service produces output:
Press ++ctrl+c++ to stop following.7. The async version¶
Everything you've seen above also works with async/await. The
AsyncSystemdClient has the exact same API -- every method is a coroutine:
import asyncio
from systemd_client import AsyncSystemdClient # (1)!
async def main():
client = AsyncSystemdClient() # (2)!
# List services -- same API, just await it
units = await client.list_units(unit_type="service") # (3)!
for unit in units:
print(f"{unit.name}: {unit.active_state}")
# Operations are coroutines too
await client.restart("my-app.service") # (4)!
# Async generator for follow
async for entry in client.journal_follow("my-app.service"): # (5)!
print(entry.message)
asyncio.run(main())
- Import
AsyncSystemdClientinstead ofSystemdClient. - Create the async client the same way -- no connection setup needed.
- Every method that was synchronous is now a coroutine. Just add
await. - Unit operations (
start,stop,restart, etc.) are all coroutines. journal_follow()becomes an async generator -- useasync forinstead offor.
When to use async?
Use AsyncSystemdClient when you're already in an async context (FastAPI,
aiohttp, or any asyncio application), or when you need to monitor
multiple services concurrently with asyncio.gather(). For simple scripts,
the sync SystemdClient is perfectly fine.
Concurrent operations with async
The real power of async is running operations in parallel:
import asyncio
from systemd_client import AsyncSystemdClient
async def main():
client = AsyncSystemdClient()
services = ["app.service", "worker.service", "scheduler.service"]
# Check all three simultaneously
results = await asyncio.gather(
*(client.is_active(svc) for svc in services)
)
for svc, active in zip(services, results):
print(f"{svc}: {'UP' if active else 'DOWN'}")
asyncio.run(main())
8. Quick CLI demo¶
systemd-client also ships with a command-line tool. You don't need to write
Python at all for common tasks:
$ systemd-client list --type service
$ systemd-client list-unit-files --state enabled
$ systemd-client status my-app.service
$ systemd-client cat my-app.service
$ systemd-client restart my-app.service
$ systemd-client start a.service b.service c.service
$ systemd-client journal -u my-app.service -n 50
$ systemd-client journal -u my-app.service --follow
For system services, add --scope system:
$ systemd-client --scope system list --type service
$ systemd-client --scope system restart nginx.service
And if you want machine-readable output, add --json:
Tip
The CLI is great for quick checks and scripting. See the full CLI Guide for all commands and options.
Next steps¶
You've covered the basics. Here's where to go from here:
- Sync Client Guide -- Full walkthrough of the synchronous API, including error handling and the deploy workflow pattern.
- Async Client Guide -- Async patterns, concurrent operations, and integration with FastAPI and aiohttp.
- Journal Guide -- Advanced journal queries, the
low-level
JournalReader, priority levels, and extra fields. - Backends Guide -- Subprocess vs. D-Bus backends, auto-detection, and when to use each.
- CLI Guide -- All CLI commands, flags, JSON output, and exit codes.
- API Reference -- Complete method signatures and type annotations.