Lightweight Python models built on dataclasses with validation, serialization, and type-safe data mapping
Project description
modmex
Lightweight Python models built on dataclasses with validation, serialization, and type-safe data mapping.
modmex gives you a small but powerful toolkit for:
- Typed models with minimal boilerplate.
- Automatic coercion and validation at initialization time.
- Recursive serialization to Python primitives and JSON.
- Per-field and per-model validation hooks.
- Type-based custom serializers to adapt output for different consumers.
Why modmex
If you want stricter models than plain dataclasses, but without the weight of a large framework, modmex is designed for that middle ground.
It focuses on:
- Simplicity: small API surface.
- Predictability: explicit model lifecycle.
- Flexibility: configurable serialization without changing your model definitions.
Installation
With pip:
pip install modmex
With Poetry:
poetry add modmex
Quick Start
from decimal import Decimal
from modmex import BaseModel, Field
class User(BaseModel):
id: int
name: str
balance: Decimal = Decimal("0")
password: str = Field("", exclude=True)
user = User(id="1", name=123, balance="10.50")
# Type coercion happens during initialization.
assert user.id == 1
assert user.name == "123"
assert user.balance == Decimal("10.50")
# model_dump returns primitive/serializable values.
assert user.model_dump() == {
"id": 1,
"name": "123",
"balance": 10.5,
}
# model_dump_json returns a JSON string.
assert user.model_dump_json() == '{"id":1,"name":"123","balance":10.5}'
Field Configuration
Use Field(...) to add serialization metadata to a model field.
Main options:
exclude=True- Always excludes this field from
model_dumpandmodel_dump_json.
- Always excludes this field from
exclude_from={"profile_name"}- Excludes this field only for selected serialization profiles.
Example:
from modmex import BaseModel, Field
class Session(BaseModel):
id: str
secret: str = Field("", exclude_from={"public"})
class User(BaseModel):
id: int
private_note: str = Field("x", exclude=True)
sessions: list[Session] = Field(default_factory=list)
user = User(id=1, sessions=[Session(id="s1", secret="abc")])
assert user.model_dump(profile="public") == {
"id": 1,
"sessions": [{"id": "s1"}],
}
Tip:
- Use
exclude=Truefor values that should never leave the model. - Use
exclude_from={...}when omission depends on the output profile.
Everyday Usage
1) Parse and normalize input data
from modmex import BaseModel
class Product(BaseModel):
id: int
name: str
active: bool
product = Product(id="10", name=123, active="true")
assert product.id == 10
assert product.name == "123"
assert product.active is True
2) Work with nested models
from modmex import BaseModel
class Address(BaseModel):
zipcode: int
class User(BaseModel):
id: int
address: Address
user = User(id="1", address={"zipcode": "90210"})
assert user.address.zipcode == 90210
3) Prepare different payloads from the same model
api_payload = account.model_dump(profile="public")
internal_payload = account.model_dump()
4) Build JSON directly
json_payload = account.model_dump_json(profile="public")
Dynamic Models
Use create_model when you need to define a model at runtime.
Field definitions can use one of these forms:
(annotation, default)(annotation, Field(...))typing.Annotated[annotation, Field(...)]
from typing import Annotated
from modmex import BaseModel, Field, create_model, field_validator, model_validator
def normalize_name(self, value: str) -> str:
return value.strip().title()
DynamicUser = create_model(
"DynamicUser",
id=(int, ...),
name=(str, Field(default="anonymous")),
slug=Annotated[str, Field(default="user", exclude=True)],
__validators__={"normalize_name": field_validator("name")(normalize_name)},
)
class StaticUser(BaseModel):
id: int
name: str = "anonymous"
slug: str = Field(default="user", exclude=True)
@field_validator("name")
def normalize_name(self, value: str) -> str:
return value.strip().title()
dynamic_user = DynamicUser(id="1", name=" john ")
static_user = StaticUser(id="1", name=" john ")
assert dynamic_user.model_dump() == static_user.model_dump()
assert dynamic_user.name == static_user.name == "Jhon"
Validators
Field validators
Use @field_validator("field_name") to transform or validate a single field.
from modmex import BaseModel, field_validator
class Product(BaseModel):
name: str
@field_validator("name")
def normalize_name(self, value: str) -> str:
return value.strip().title()
Model validators
Use @model_validator(mode="before" | "after") to work with full model state.
before: runs before type coercion.after: runs after field-level validation.
from modmex import BaseModel, model_validator
class Product(BaseModel):
name: str
slug: str = ""
@model_validator(mode="before")
def build_slug(self, values: dict) -> dict:
values["slug"] = values["name"].lower().replace(" ", "-")
return values
Serialization
model_dump(...)
Use model_dump when you need a dictionary payload.
Most common options:
exclude={...}to omit fields for a specific call.profile="..."to applyexclude_fromrules.include_excluded=Trueto force metadata-excluded fields into the payload.type_serializers={...}to control how specific Python types are represented.
model_dump_json(...)
Use model_dump_json when you need a JSON string output.
It supports the same practical options as model_dump (exclude, profile, include_excluded, type_serializers).
Omitting Fields During Serialization
Use this feature when the same model must produce different payloads depending on where the data is going.
exclude_fromdefines where a field should be omitted.profileselects which omission rules to apply in a specific dump call.
What each option does
exclude_from={"public"}- Omit this field when serializing with
profile="public".
- Omit this field when serializing with
profile="public"- Apply all field rules tagged for
publicduring serialization.
- Apply all field rules tagged for
Common pattern
You may want one shape for API responses and another for internal flows (logs, queues, exports, persistence payloads, etc.).
- API payload (
profile="public"): hide internal fields. - Internal payload (no profile, or another profile): keep those fields.
Example
from modmex import BaseModel, Field
class Account(BaseModel):
id: int
email: str = Field("", exclude_from={"public"})
internal_note: str = Field("", exclude=True)
account = Account(id=1, email="a@x.com", internal_note="secret")
# No profile: only always-excluded fields are removed.
assert account.model_dump() == {
"id": 1,
"email": "a@x.com",
}
# public profile: profile-based exclusions are applied.
assert account.model_dump(profile="public") == {
"id": 1,
}
# include_excluded=True: ignore Field exclusion metadata.
assert account.model_dump(profile="public", include_excluded=True) == {
"id": 1,
"email": "a@x.com",
"internal_note": "secret",
}
# Dynamic omission for one call (without metadata changes).
assert account.model_dump(exclude={"email"}) == {
"id": 1,
}
Type-Based Custom Serializers
You can override serialization behavior by type with type_serializers.
Shape:
type_serializers = {
SomeType: serializer_function,
}
Keep Decimal values as Decimal in model_dump
from decimal import Decimal
dumped = model.model_dump(
type_serializers={
Decimal: lambda value: value,
}
)
Convert float to Decimal for a specific output contract
from decimal import Decimal
from modmex import BaseModel
class Price(BaseModel):
amount: float
p = Price(amount=10.25)
dumped = p.model_dump(
type_serializers={
float: lambda value: Decimal(str(value)),
}
)
assert dumped["amount"] == Decimal("10.25")
Emit Decimal as string in JSON
from decimal import Decimal
dumped_json = model.model_dump_json(
type_serializers={
Decimal: lambda value: str(value),
}
)
Note: some client libraries expect Decimal instead of float values (for example, common boto3 workflows). Type serializers let you adapt output contracts cleanly, without hard-coding backend-specific behavior into your models.
Error Handling
Validation issues raise ValidationError.
Each error includes:
loc: location path (supports nested structures).msg: human-readable message.type: error category.
Example locations:
["address", "zipcode"]["tags", 1]
Practical Usage Pattern
Use this rule of thumb:
- Keep rich Python types in the in-memory model instance.
- Use
model_dump/model_dump_jsonto produce transport-friendly payloads. - Use
type_serializerswhen a specific consumer requires a different type format.
Compatibility
- Python 3.10+
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file modmex-1.1.8.tar.gz.
File metadata
- Download URL: modmex-1.1.8.tar.gz
- Upload date:
- Size: 26.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
182998289035cc4e383c71c8cea8f4972afdecbc149962cca866208f34d0a139
|
|
| MD5 |
4ceaa52b5e262a1ad8d2a72c4e5574c7
|
|
| BLAKE2b-256 |
c0f29a58823165010794e17f3ca6361ae3c0d94e1a6447d782d0aa219a1a23cf
|
Provenance
The following attestation bundles were made for modmex-1.1.8.tar.gz:
Publisher:
release.yml on modmex/modmex
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
modmex-1.1.8.tar.gz -
Subject digest:
182998289035cc4e383c71c8cea8f4972afdecbc149962cca866208f34d0a139 - Sigstore transparency entry: 1721987758
- Sigstore integration time:
-
Permalink:
modmex/modmex@ae11c2db643cd6226823c778de4c3a1302269bcb -
Branch / Tag:
refs/tags/v1.1.8 - Owner: https://github.com/modmex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ae11c2db643cd6226823c778de4c3a1302269bcb -
Trigger Event:
push
-
Statement type:
File details
Details for the file modmex-1.1.8-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: modmex-1.1.8-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 175.8 kB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f2460bc835844f1789dd96ea7dba01cdd1b1db4f7571208b5af9c9bda24907c2
|
|
| MD5 |
9aa8a3c4410da36f3c81dbb585b5b84b
|
|
| BLAKE2b-256 |
fb82c4acf0e615d3402d59f006631e82a1ae43c88b55cf1f593e5c62431601da
|
Provenance
The following attestation bundles were made for modmex-1.1.8-cp310-abi3-win_amd64.whl:
Publisher:
release.yml on modmex/modmex
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
modmex-1.1.8-cp310-abi3-win_amd64.whl -
Subject digest:
f2460bc835844f1789dd96ea7dba01cdd1b1db4f7571208b5af9c9bda24907c2 - Sigstore transparency entry: 1721987940
- Sigstore integration time:
-
Permalink:
modmex/modmex@ae11c2db643cd6226823c778de4c3a1302269bcb -
Branch / Tag:
refs/tags/v1.1.8 - Owner: https://github.com/modmex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ae11c2db643cd6226823c778de4c3a1302269bcb -
Trigger Event:
push
-
Statement type:
File details
Details for the file modmex-1.1.8-cp310-abi3-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: modmex-1.1.8-cp310-abi3-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 268.6 kB
- Tags: CPython 3.10+, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6062ddbff652e5f24dda44d304f8fcc00118afd9e9bf5a7adc4a4ec03e3a183e
|
|
| MD5 |
14549c009063cd69786e3084464e9d40
|
|
| BLAKE2b-256 |
f2302761653b41a0b5ca016fb4972339fa3ea688aeb68b3c4bb789993452162b
|
Provenance
The following attestation bundles were made for modmex-1.1.8-cp310-abi3-manylinux_2_34_x86_64.whl:
Publisher:
release.yml on modmex/modmex
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
modmex-1.1.8-cp310-abi3-manylinux_2_34_x86_64.whl -
Subject digest:
6062ddbff652e5f24dda44d304f8fcc00118afd9e9bf5a7adc4a4ec03e3a183e - Sigstore transparency entry: 1721988089
- Sigstore integration time:
-
Permalink:
modmex/modmex@ae11c2db643cd6226823c778de4c3a1302269bcb -
Branch / Tag:
refs/tags/v1.1.8 - Owner: https://github.com/modmex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ae11c2db643cd6226823c778de4c3a1302269bcb -
Trigger Event:
push
-
Statement type:
File details
Details for the file modmex-1.1.8-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: modmex-1.1.8-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 245.0 kB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2dfec90c0f6be726048049fb630b48e21af2ab4f47948e93c890a4ef67097713
|
|
| MD5 |
8dba42312db1880d66eca519ff0bb970
|
|
| BLAKE2b-256 |
d1d5160c586eb65f7126bfbc3c15ad95fbb16d84ce49d8c3bc27409f0a01c639
|
Provenance
The following attestation bundles were made for modmex-1.1.8-cp310-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on modmex/modmex
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
modmex-1.1.8-cp310-abi3-macosx_11_0_arm64.whl -
Subject digest:
2dfec90c0f6be726048049fb630b48e21af2ab4f47948e93c890a4ef67097713 - Sigstore transparency entry: 1721987828
- Sigstore integration time:
-
Permalink:
modmex/modmex@ae11c2db643cd6226823c778de4c3a1302269bcb -
Branch / Tag:
refs/tags/v1.1.8 - Owner: https://github.com/modmex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ae11c2db643cd6226823c778de4c3a1302269bcb -
Trigger Event:
push
-
Statement type: