Contents
Chapter 18

Messenger

The Messenger or Data Transfer Object is a way to pass a clump of information around. The most typical place for this is in return values from functions, where tuples or dictionaries are often used. However, those rely on indexing; in the case of tuples this requires the consumer to keep track of numerical order, and in the case of a dict you must use the d["name"] syntax which can be slightly less desirable.

A Messenger is simply an object with attributes corresponding to the names of the data you want to pass around or return:

# messenger_idiom.py
from typing import Any


class Messenger:
    def __init__(self, **kwargs: Any) -> None:
        self.__dict__ = kwargs


m: Any = Messenger(info="some information", b=['a', 'list'])
m.more = 11
print(m.info, m.b, m.more)

The trick here is that the __dict__ for the object is just assigned to the dict that is automatically created by the **kwargs argument.

You could create a Messenger class and put it in a library to import. But it takes so few lines that defining it in-place, wherever you need it, usually makes more sense.

The Standard-Library Versions

You rarely need even those few lines, because Python already ships this idiom. types.SimpleNamespace is exactly a Messenger: keyword arguments become attributes. When you want the fields named and type-checked, a @dataclass gives you a typed mutable record with a generated __init__, repr, and equality, and a NamedTuple gives you a typed immutable one:

# messenger_modern.py
# The standard library already provides this idiom and its typed
# cousins.
from dataclasses import dataclass
from types import SimpleNamespace
from typing import NamedTuple

# SimpleNamespace is exactly the Messenger idiom, built in:
m = SimpleNamespace(info="some information", b=["a", "list"])
m.more = 11
print(m.info, m.b, m.more)


# A dataclass is the typed, mutable version:
@dataclass
class Point:
    x: float
    y: float


print(Point(1.0, 2.0))


# A NamedTuple is the typed, immutable version:
class Color(NamedTuple):
    r: int
    g: int
    b: int


print(Color(255, 0, 0).r)

Use SimpleNamespace for an ad-hoc bag of attributes, a @dataclass for a typed mutable record, and a NamedTuple for a typed immutable one. Write the hand-rolled Messenger only to show how SimpleNamespace works underneath. To make a @dataclass guarantee that its values are legal, not merely typed, see Data Classes as Types.

A small test confirms each form behaves as a record: the hand-rolled Messenger turns keyword arguments into attributes (and takes new ones later), the @dataclass carries fields and value equality, and the NamedTuple is a named record you can still treat as a tuple:

# test_messenger.py
from typing import Any

from messenger_idiom import Messenger
from messenger_modern import Color, Point


def test_messenger_exposes_kwargs_as_attributes() -> None:
    m: Any = Messenger(info="hi", count=3)
    assert m.info == "hi"
    assert m.count == 3
    m.added = 9  # attributes can be added afterward, too
    assert m.added == 9


def test_dataclass_point_has_fields_and_equality() -> None:
    assert Point(1.0, 2.0).x == 1.0
    assert Point(1.0, 2.0) == Point(1.0, 2.0)
    assert Point(1.0, 2.0) != Point(1.0, 3.0)


def test_namedtuple_color_is_a_named_record() -> None:
    c = Color(255, 0, 0)
    assert c.r == 255
    assert (c.r, c.g, c.b) == tuple(c)