Return Many Values as Attributes in Python

Janne Kemppainen |

When you need to return complex data from a function you typically think of two options:

  1. put the values in a dictionary
  2. create a new object/class

The first option is simple to implement but you need to access the individual values by their keys. The second option allows you to access data via attributes and do custom calculations behind the scenes, but then you need to implement yet another class.

Is there something in Python that could give us easy attribute access without having to bother with custom classes?

SimpleNamespace is a handy wrapper class that we can use to build anonymous objects that can contain anything you want. When you construct a SimpleNamespace object you provide the keyword arguments that you want to have available as attributes, and you can add or remove attributes during the lifetime of the construct.

Note that you cannot use an object for this purpose because trying to add new attributes to an object will cause an AttributeError.

>>> a = object()
>>> a.my_val = 123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'my_val'

Let's demonstrate this with an imaginary example:

You need a program that stores user data in a structured format. The input data contains strings of full names. You want to make a best effort guess to parse the first and last names, and possible middle names from this data. Implement a helper function called parse_name for your program.

Let's ignore the fact that people can have more than one last name to keep things simpler. Using SimpleNamespace the implementation could look like this:

# code/names.py
from types import SimpleNamespace


def parse_name(name: str):
    if not name or not name.strip():
        raise ValueError("A name must be provided")
    parts = name.split()
    first_name = parts[0]
    middle_names = " ".join(parts[1:-1])
    family_name = parts[-1] if len(parts) > 1 else ""
    return SimpleNamespace(
        first_name=first_name, 
        middle_names=middle_names, 
        family_name=family_name
    )

The function takes the name input and checks that it isn't None, an empty string, or whitespace. Then it splits the input using whitespace characters, uses the first item as the first name, uses the last item as the family name (if more than one item in the list), and sets anything in the middle as middle names.

The return value is a SimpleNamespace object that contains the parsed names. Each part is available as an attribute as you can see from the unit tests:

# tests/test_names.py
import unittest
from code.names import parse_name


class TestNames(unittest.TestCase):
    def test_parse_first_name_only(self):
        result = parse_name("Janne")
        self.assertEqual("Janne", result.first_name)
        self.assertEqual("", result.family_name)
        self.assertEqual("", result.middle_names)

    def test_parse_first_name_and_family_name(self):
        result = parse_name("Tom Hanks")
        self.assertEqual("Tom", result.first_name)
        self.assertEqual("Hanks", result.family_name)
        self.assertEqual("", result.middle_names)
    
    def test_parse_full_name(self):
        result = parse_name("Kiefer William Frederick Dempsey George Rufus Sutherland")
        self.assertEqual("Kiefer", result.first_name)
        self.assertEqual("Sutherland", result.family_name)
        self.assertEqual("William Frederick Dempsey George Rufus", result.middle_names)

    def test_no_name(self):
        with self.assertRaises(ValueError):
            parse_name("")
        with self.assertRaises(ValueError):
            parse_name(" ")

If you run the tests with python3 -m unittest all four of them should pass.

You probably don't want to return this object when others are using your code, then it is usually better to define the API through a class. I think that SimpleNamespace can be considered for cases where you call internal code that needs to return multiple values.

Attributes can be added or removed after the object has been created. Therefore this refactored version passes the tests too:

def parse_name(name: str):
    if not name or not name.strip():
        raise ValueError("A name must be provided")
    parts = name.split()
    names = SimpleNamespace()
    names.first_name = parts[0]
    names.middle_names = " ".join(parts[1:-1])
    names.family_name = parts[-1] if len(parts) > 1 else ""
    return names

I think accessing the results this way is a bit nicer than a dict return value:

# SimpleNamespace
result.first_name
# dict
result["first_name"]

The third option that I haven't mentioned so far is to just return multiple values (part of the logic is omitted):

def parse_name(name: str):
  ...
  return first_name, middle_names, family_name

first_name, middle_names, family_name = parse_name(name_input)

If you don't need one of the options you'd have to use _ as a placeholder for that value since you need an equal amount of entries on both sides to unpack a tuple to variables. With a larger amount of return values this becomes inconvenient to maintain.

first_name, _, last_name = parse_name(name_input)

The multiple return values can be stored to a single variable and accessed through their indices:

name_parts = parse_name(name_input)
first_name = name_parts[0]
family_name = name_parts[2]

If you add more return values then this solution becomes brittle as the indices can change. You could put the values into a namedtuple but would be practically equal to a dictionary.

So in conclusion, when you need multiple return values for internal code consider using the SimpleNamespace object to pass them around as that gives you nice attribute access to the values. If your code is used by others then you should definitely define the interface properly with a class.

Have you used SimpleNamespace somewhere? Do you know of good alternative use cases? Share your thoughts on Twitter!

Discuss on Twitter

Subscribe to my newsletter

What's new with PäksTech? Subscribe to receive occasional emails where I will sum up stuff that has happened at the blog and what may be coming next.

powered by TinyLetter | Privacy Policy