The Value Object Pattern is a Python π package for building immutable, self-validating value objects π¦. It helps
move validation, normalization, primitive conversion, and domain-specific constraints out of scattered application code
and into small typed objects that can be reused across models, services, APIs, and tests.
- π₯ Installation
- π Documentation
- β‘ Quick Start
- π§© Core Ideas
- π€ Why Value Objects?
- π¦ Core Models
- β Reusable Value Objects
- π Primitive Conversion
- π€ Contributing
- π License
You can install Value Object Pattern using pip:
pip install value-object-patternYou can install the companion AI-agent skill from skills.sh with Vercel's skills CLI:
npx skills add adriamontoto/value-object-patternReview the skill source in skills/value-object-pattern before installing it in
sensitive environments.
The root README is the entry point. Deeper guides live in this repository and are linked here:
docs/README.md: Documentation hub.docs/usage/README.md: Custom value objects, validators, processors, and model composition.docs/catalog/README.md: Feature map by reusable value-object category.- Catalog details:
primitives,dates,identifiers,internet, andmoney. docs/conversion/README.md: Primitive conversion and nested model behavior.AI Skill: Installable skill package that teaches AI agents how to use Value Object Pattern.
This project's DeepWiki documentation is also available for generated repository navigation.
Create a value object by subclassing ValueObject[T] and adding validation hooks:
from value_object_pattern import ValueObject, validation
class Age(ValueObject[int]):
@validation(order=0)
def _ensure_value_is_integer(self, value: int) -> None:
if type(value) is not int:
raise TypeError('Age value must be an integer.')
@validation(order=1)
def _ensure_value_is_positive(self, value: int) -> None:
if value <= 0:
raise ValueError('Age value must be positive.')
age = Age(value=42)
print(age.value)
# >>> 42
print(repr(age))
# >>> Age(value=42)Use reusable value objects when the package already provides the constraint:
from value_object_pattern.usables import NotEmptyStringValueObject, PositiveIntegerValueObject
name = NotEmptyStringValueObject(value='Ada')
quantity = PositiveIntegerValueObject(value=3)Value objects in this package are designed around a few consistent rules:
- They wrap exactly one value and expose it through
.value. - They validate input during construction.
- They can normalize input with
@processhooks. - They reject attribute mutation after construction.
- They compare by concrete class and wrapped value.
- They can customize validation error context with
titleandparameter.
from value_object_pattern import process
from value_object_pattern.usables import StringValueObject
class LowerTrimmedName(StringValueObject):
@process(order=0)
def _trim(self, value: str) -> str:
return value.strip()
@process(order=1)
def _lower(self, value: str) -> str:
return value.lower()
name = LowerTrimmedName(value=' ADA ')
assert name.value == 'ada'Value objects make domain rules explicit. A plain str, int, or dict can carry almost anything, so every function
that receives it has to remember what "valid" means. A value object gives that rule a name and enforces it at the point
where the value enters the model.
Without a value object, the same rule tends to be repeated across services, controllers, tests, and serializers:
def register_user(email: str, age: int) -> None:
if '@' not in email:
raise ValueError('Invalid email.')
if age <= 0:
raise ValueError('Invalid age.')With value objects, the signature communicates the expected shape and invalid values are rejected before the rest of the code depends on them:
from value_object_pattern.usables import PositiveIntegerValueObject
from value_object_pattern.usables.internet import EmailAddressValueObject
def register_user(email: EmailAddressValueObject, age: PositiveIntegerValueObject) -> None:
assert '@' in email.value
assert age.value > 0This is useful when a value has a reusable meaning: email address, positive quantity, trimmed name, country code, URL,
UUID, money identifier, or any project-specific concept such as TenantSlug, OrderLimit, or CustomerName.
Raw literals and primitives are still the right choice when the value is simple or intentionally exact:
- Use raw primitives inside low-level calculations where no domain rule is being expressed.
- Use hardcoded values for exact examples, snapshots, JSON, SQL, URLs, error messages, and public documentation output.
- Use explicit boundary values for deliberate limits such as zero, one, minimum length, maximum length, empty strings, first date, last date, or a known invalid format.
- Use
.valuewhen crossing into libraries, APIs, or storage layers that expect primitives.
from value_object_pattern.usables import PositiveIntegerValueObject
quantity = PositiveIntegerValueObject(value=10)
assert quantity.value == 10
assert quantity.value + 5 == 15The practical rule is simple: use value objects for named domain constraints, and use primitives for exact literals, low-level operations, and boundary examples.
| Model | Purpose |
|---|---|
ValueObject[T] |
Base class for immutable validated single-value wrappers. |
EnumerationValueObject[E] |
Stores enum members while accepting enum members or raw enum values. |
UnionValueObject[T] |
Accepts and converts values that match a union annotation; supports subclass and inline construction. |
BaseModel |
Adds representation, equality, copying, and primitive conversion for aggregate-like models. |
ListValueObject[T] |
Immutable typed list wrapper; supports subclass and inline construction. |
DictValueObject[K, V] |
Immutable typed dictionary wrapper; supports subclass and inline construction. |
See docs/usage/README.md for examples of each model.
The package includes reusable validators for common shapes:
| Category | Examples |
|---|---|
| Primitives | strings, bytes, booleans, integers, floats, None / not-None |
| String formats | non-empty, trimmed, alpha, alphanumeric, lower/upper case, snake case, kebab case, secret strings |
| Dates | date, datetime, date strings, datetime strings, timezone objects, timezone names |
| Identifiers | UUIDs and UUID strings, world codes, Spanish identifiers and vehicle plates |
| Internet | URLs, hosts, domains, ports, emails, IP addresses, networks, MAC addresses, slugs, keys |
| Money | IBANs and credit card values |
See docs/catalog/README.md for import paths and category guidance.
BaseModel, ListValueObject, DictValueObject, and UnionValueObject can convert between primitive data and richer
types.
from value_object_pattern import BaseModel
from value_object_pattern.usables import NotEmptyStringValueObject, PositiveIntegerValueObject
class User(BaseModel):
def __init__(self, name: NotEmptyStringValueObject, age: PositiveIntegerValueObject) -> None:
self.name = name
self.age = age
user = User.from_primitives(primitives={'name': 'Ada', 'age': 42})
assert isinstance(user.name, NotEmptyStringValueObject)
assert user.to_primitives() == {'age': 42, 'name': 'Ada'}More details are available in docs/conversion/README.md.
We love community help! Before you open an issue or pull request, please read:
Thank you for helping make π¦ Value Object Pattern package awesome! π
This project is licensed under the terms of the MIT license.