Mastering the Value Object Pattern in Python
The Trap of Primitive Obsession
Developers often default to
Defining the Value Object
Originating from
Implementation via Dataclasses
The most straightforward way to implement this pattern is using frozen=True, you ensure immutability.
from dataclasses import dataclass
@dataclass(frozen=True)
class Price:
value: float

def __post_init__(self):
if self.value < 0:
raise ValueError("Price must be non-negative")
In this model, the `__post_init__` method acts as a gatekeeper. Any attempt to instantiate a `Price` with a negative number immediately raises a `ValueError`.
## Inheriting from Primitives
If accessing `.value` feels cumbersome, you can inherit directly from [float](entity://types/float). Since floats are already immutable, this preserves the value object's integrity while allowing the object to behave like a standard number.
```python
class Percentage(float):
def __new__(cls, value):
if not 0 <= value <= 1:
raise ValueError("Percentage must be between 0 and 1")
return super().__new__(cls, value)
Using __new__ allows you to validate the data before the object is even created. This allows you to perform math operations directly on the object without manually extracting the internal value.
Practical Syntax Notes
- Immutability: Always use
frozen=Truewith dataclasses to prevent state changes after creation. - Custom Creators: Use
@classmethodfor alternative constructors, such asfrom_percent(cls, value)which might divide an integer by 100. - Type Clarity: Value objects act as documentation. A function signature requiring a
EmailAddresstype is far more descriptive than one requiring astr.

Fancy watching it?
Watch the full video and context