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:
- put the values in a dictionary
- 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!
Read next in the Python bites series.
Discuss on Twitter
The SimpleNamespace class can become handy when you need to return multiple values from a function in Python. What other use cases have you found, or is this the first time you hear about it?https://t.co/50IB4TKy8h
— Janne Kemppainen (@pakstech) January 13, 2021
Previous post
Import ES6 Modules in Node.js