2025, Oct 26 05:00

How to Fix NetworkManager AddAndActivateConnection a{sa{sv}} Errors in Python with pydbus (GLib.Variant, SSID ay)

Fix NetworkManager AddAndActivateConnection a{sa{sv}} TypeError in Python: wrap inner values as GLib.Variant and send Wi‑Fi SSID as ay bytes using pydbus

Working with NetworkManager over D-Bus is straightforward until you hit the wall of type signatures. One of the common stumbling blocks is AddAndActivateConnection and its first parameter defined as a{sa{sv}}. If you pass a plain Python dict with strings inside, GLib will complain with a TypeError saying it expected a GLib.Variant but got str. Here is what is actually happening and how to fix it without guesswork.

Reproducing the issue

The following code discovers a Wi‑Fi device, assembles a connection profile, and tries to activate it. The structure looks fine from a Python point of view, but it fails due to D-Bus typing rules.

from pydbus import SystemBus

sys_bus = SystemBus()
nmgr = sys_bus.get("org.freedesktop.NetworkManager")

# Find Wi-Fi device (type 2)
wlan_objpath = None
for obj_path in nmgr.GetDevices():
    dev_obj = sys_bus.get("org.freedesktop.NetworkManager", obj_path)
    if dev_obj.DeviceType == 2:
        wlan_objpath = obj_path
        break

if not wlan_objpath:
    raise RuntimeError("No Wi-Fi device found.")

net_ssid = "MyWiFi"
net_pass = "mypassword123"

raw_settings = {
    'connection': {
        'id': net_ssid,
        'type': '802-11-wireless',
    },
    '802-11-wireless': {
        'ssid': net_ssid,
        'mode': 'infrastructure',
    },
    '802-11-wireless-security': {
        'key-mgmt': 'wpa-psk',
        'psk': net_pass,
    },
    'ipv4': {
        'method': 'auto',
    },
    'ipv6': {
        'method': 'ignore',
    }
}

nmgr.AddAndActivateConnection(raw_settings, wlan_objpath, "/")

At runtime this leads to a GLib error. The key line typically looks like this:

TypeError: argument value: Expected GLib.Variant, but got str

Why this fails

The signature a{sa{sv}} matters. It describes a mapping from strings to another mapping whose values are variants. The outer dict is a dictionary of setting groups. Each inner dict is a dictionary of setting keys, and the values in those inner dicts must be of type variant. Python’s dict has no type information, so every value must be explicitly wrapped in a GLib.Variant to satisfy the D-Bus signature. Even if every value is a string, a plain a{sa{ss}} is not the same as a{sa{sv}}, and the call will be rejected when pydbus constructs the GLib.Variant tree.

It is tempting to wrap the entire settings dict in a single Variant, but that does not solve the mismatch. Doing so would create a v at the top level rather than a{sa{sv}}, and the inner values would still not be per-key variants with their own types. Moreover, even inside a variant, the contained structure must still be representable in D-Bus types, so this shortcut does not help.

There is another detail that bites many users: the 802-11-wireless.ssid field must be of type ay (array of bytes), not a string. Passing a string variant for ssid will result in another error. The correct representation is a bytearray wrapped in Variant('ay', ...).

The fix

Wrap every inner value as a GLib.Variant with the correct type. For string fields use 's'. For SSID use 'ay' with a bytearray of the UTF‑8 bytes. The outer and inner dicts remain normal Python dicts, but all leaf values must be variants.

from pydbus import SystemBus
from gi.repository.GLib import Variant

sys_bus = SystemBus()
nmgr = sys_bus.get("org.freedesktop.NetworkManager")

# Find Wi-Fi device (type 2)
wlan_objpath = None
for obj_path in nmgr.GetDevices():
    dev_obj = sys_bus.get("org.freedesktop.NetworkManager", obj_path)
    if dev_obj.DeviceType == 2:
        wlan_objpath = obj_path
        break

if not wlan_objpath:
    raise RuntimeError("No Wi-Fi device found.")

net_ssid = "MyWiFi"
net_pass = "mypassword123"

fixed_settings = {
    "connection": {
        "id": Variant('s', net_ssid),
        "type": Variant('s', "802-11-wireless"),
    },
    "802-11-wireless": {
        "ssid": Variant('ay', bytearray(net_ssid, 'utf-8')),
        "mode": Variant('s', "infrastructure"),
    },
    "802-11-wireless-security": {
        "key-mgmt": Variant('s', "wpa-psk"),
        "psk": Variant('s', net_pass),
    },
    "ipv4": {"method": Variant('s', "auto")},
    "ipv6": {"method": Variant('s', "ignore")},
}

nmgr.AddAndActivateConnection(fixed_settings, wlan_objpath, "/")

What’s really going on under the hood

Dicts carried over D-Bus are strongly typed. That is why APIs often declare option maps as having variant values: variants carry the real type while keeping the outer structure uniform. GLib.Variant mirrors the D-Bus variant closely, so the same format strings apply. In this context, pydbus builds the GLib.Variant tree based on the introspection signature and therefore insists that the inner values already be variants. Some calls that read a{sv} may give you a plain Python dict, but creating a{sv} for sending is stricter and requires explicit wrapping. If you obtain settings from a getter, then change a field and send them back, you need to re-wrap the modified leaves as variants before the update call, otherwise you will hit the same type error again.

Why you want to understand this

Once you start composing NetworkManager settings programmatically, mismatched types are the fastest way to lose time. Knowing that a{sa{sv}} means “inner values must be variants” lets you shape your data correctly upfront. It also prevents subtle bugs when mixing reads and writes: a getter may hand you a regular Python structure, but a setter expects typed variants for the same slots.

Takeaways

Always follow the D-Bus signature from introspection. For AddAndActivateConnection, the outer and inner mappings are plain dicts, but every actual value inside the inner dicts must be a GLib.Variant of the proper type. Do not wrap the entire settings map in a single Variant; it won’t match a{sa{sv}}. Ensure that 802-11-wireless.ssid is Variant('ay', bytearray(...)) rather than a string. And when modifying settings retrieved from another call, reapply Variant wrappers to the fields you change before sending them back.

The article is based on a question from StackOverflow by owndampu and an answer by grawity.