Implementing Value Objects in Python
Tags: python, domaindrivendesign, ddd
Published: 2021-12-01 09:18:13

A [**Value Object**](https://en.wikipedia.org/wiki/Value_object) is one of the fundamental building blocks of Domain-Driven Design. It is a small object (in terms of memory), which consists of one or more attributes, and which represents a conceptual whole. Value Object is usually a part of *Entity*.

Some examples of value objects are: `Email` (consisting of a single email attribute), `Money` (consisting of amount and currency), `DateRange` (consisting of a start date, and an end date), `GPSCoordinates` (made of latitude and longitude), or `Address` (consisting of a street, zip code, city, state, etc.). Apart from the attributes, all of the above can (and should) include some kind of validation logic too.

As you can see from the examples above, value objects do not have an identity - they are simply a collection of attributes that are related to each other.

Here are the most important properties of a value object:
1. Its state is immutable. Once created, the state of a value object cannot be changed.
2. It is distinguishable only by the state of its attributes. Two instances with the same attribute values are considered to be equal (this is also known as *structural equality*).
3. It should encapsulate business logic that prevents us from constructing a value object with an invalid state (i.e. start date < end date for a date range).
4. All methods of a value object should be [*pure*](https://en.wikipedia.org/wiki/Pure_function), i.e. calling a method does not trigger any side effects or change the state of a value object. However, returning a new instance that reflects the changes is fine.
5. It should be easy to unit-test a value object and it should be easy to reason about its logic. 

To recognize a value object in your domain model, mentally replace it with a tuple with some validation logic and a few extra methods that are easy to test.

Let's implement a `DateRange` value object using Python `dataclasses` module:

```python
from dataclasses import dataclass
from datetime import date

class BusinessRuleValidationException(Exception):
  """A base class for all business rule validation exceptions"""

class ValueObject:
  """A base class for all value objects"""

@dataclass(frozen=True)
class DateRange(ValueObject):
  """Our first value object"""
  start_date: date
  end_date: date

  def __post_init__(self):
    """Here we check if a value object has a valid state."""
    if not self.start_date < self.end_date
      raise BusinessRuleValidationException("end date date should be greater than start date")

  def days(self):
    """Returns the number of days between the start date and the end date"""
    delta = self.end_date - self.start_date + timedelta(days=1)
    return delta.days

  def extend(self, days):
    """Extend the end date by a specified number of days"""
	new_end_date = self.end_date + timedelta(days=days)
	return DateRange(self.start_date, new_end_date)
```

**Re 1**:  To guarantee immutability of a `DateRange`, we are using `@dataclass(frozen=True)` decorator.

**Re 2**: Equality is guaranteed by a `dataclass` itself, which compares the class instance as if it were a tuple of its fields.

**Re 3**: We validate the state of an instance in `__post_init__` method using simple logic to check invariants. It prevents us from creating an invalid date range.

**Re 4**: Our value object has only 2 methods: `days` and `extend`. Both of them are pure (they are side effects free). Note that `extend` returns a new instance of `DateRage` instead of modifying the `end_date` attribute.

**Re 5**: Thanks to its simple behavior, unit testing `DateRage` is also relatively straightforward:

```python
import unittest

class DateRangeTestCase(unittest.TestCase):
    def test_equality(self):
	    range1 = DateRange(start_date=date(2020,1,1), end_date=date(2021,1,1))
		range2 = DateRange(start_date=date(2020,1,1), end_date=date(2021,1,1))
        self.assertEqual(range1, range2)

	def test_days(self):
		range = DateRange(start_date=date(2020,1,1), end_date=date(2020,1,1))
		self.assertEqual(range.days(), 1)

	def test_days(self):
		range1 = DateRange(start_date=date(2020,1,1), end_date=date(2020,1,1))
		range2 = range1.extend(days=1)
		self.assertEqual(
			range2, 
			DateRange(start_date=date(2020,1,1), end_date=date(2021,1,2))
		)

    def test_cannot_create_invalid_date_range(self):
        with self.assertRaises(BusinessRuleValidationException):
    	    DateRange(start_date=date(2021,1,1), end_date=date(2020,1,1))
```

Using value objects in your code will also help you in fighting with [primitive obsession](https://refactoring.guru/smells/primitive-obsession). Why is it important? Let me give you an example to illustrate the problem. Let's say that you decide to cut corners and use `string` to represent emails. There is a high chance that you will need to validate those emails as well, and most likely you will need to do it in multiple places (i.e. user inputs, form data, serializers, business logic, etc.). Having a simple `Email` value object will help you to stay DRY in the long run.

***

*This article was first published in [DDD in Python](https://dddinpython.com/index.php/2021/11/22/value-objects-in-python/).*