Value Objects in Python — Core Concepts

What value objects are

A value object is an object defined entirely by its attributes rather than by a unique identity. Two value objects with the same attributes are considered equal. They are immutable — once created, they do not change.

This contrasts with entities, which have a unique identity. Two customers named “Alice” at the same address might be different people (different IDs). But two Money(100, "USD") objects are always the same thing.

The three properties

1. Equality by value

address_a = Address("123 Main St", "Springfield", "IL")
address_b = Address("123 Main St", "Springfield", "IL")
assert address_a == address_b  # True — same values

This is different from the default Python behavior where two objects are equal only if they are the exact same instance in memory.

2. Immutability

Value objects cannot be modified after creation. If you need a different value, you create a new object.

price = Money(Decimal("19.99"), "USD")
# price.amount = Decimal("29.99")  ← This should fail
new_price = price.with_amount(Decimal("29.99"))  # Create a new object instead

Immutability prevents accidental side effects. If you pass a Money object to a function, you know the function cannot change your copy.

3. Self-validation

Value objects validate their data at creation time. An invalid value object should never exist.

Money(Decimal("-5"), "USD")    # Should raise ValueError
EmailAddress("not-an-email")   # Should raise ValueError

Implementing value objects in Python

from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Amount cannot be negative")
        if len(self.currency) != 3:
            raise ValueError("Currency must be 3-letter ISO code")

    def add(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError(f"Cannot add {self.currency} to {other.currency}")
        return Money(self.amount + other.amount, self.currency)

The frozen=True parameter makes the dataclass immutable and automatically generates __eq__ and __hash__ based on all fields.

NamedTuple

from typing import NamedTuple

class Coordinates(NamedTuple):
    latitude: float
    longitude: float

Named tuples are inherently immutable and support equality comparison. They are simpler than dataclasses but lack custom validation in __post_init__.

Value objects vs plain data

You might wonder: why not just use a plain dictionary or tuple? Value objects add:

  • Type safetyMoney and Weight are different types even if both hold a number
  • Validation — Invalid states are impossible
  • Behavior — Methods like add() and multiply() that know the domain rules
  • Documentation — The class name and fields communicate intent

Common value objects in Python projects

Value ObjectFieldsWhy not a primitive?
Moneyamount, currencyPrevents mixing currencies
EmailAddressaddressValidates format at creation
DateRangestart, endEnsures end > start
PercentagevalueEnsures 0-100 range
PhoneNumbercountry_code, numberValidates format
GeoCoordinatelat, lonValidates ranges

Common misconception

“Value objects are just DTOs (Data Transfer Objects).” DTOs carry data between layers without behavior. Value objects carry data with behavior and invariants. A Money value object knows how to add amounts and prevent currency mixing. A DTO just shuttles fields.

When to use value objects

Use them whenever you find yourself:

  • Passing around related fields together (amount + currency, latitude + longitude)
  • Validating the same rules in multiple places
  • Using primitive types that could be confused (is this float a price, a weight, or a distance?)

The overhead is minimal in Python. A frozen dataclass is one decorator and a few lines.

The one thing to remember: Value objects replace scattered primitives with self-validating, immutable types that make your domain concepts explicit and your code resistant to a whole class of bugs.

pythonarchitectureddd

See Also