🧬 simplibs-object
A system for building deterministic, type-safe, and reactive objects.
simplibs-object is a library for declarative data modeling.
Instead of writing constructors, setters, validators, and manually recalculating dependencies, you define a class and tell it what type it has, what its default value is, what it's composed of, and how the resulting value is built from those parts. The library takes care of the rest.
pip install simplibs-object
The library is built on three simple building blocks:
SimpleConstant— an atomic value,SimpleObject— a composition of other objects,SimpleMixin— a capability that can be added to an object.
These blocks can be combined into simple objects as well as larger structures.
For example, you might write:
class Age(SimpleConstant):
_type = int
_default = 20
or:
class Person(SimpleObject):
_type = str
_inners = (FirstName, LastName)
@classmethod
def _logic(cls, first_name, last_name):
return f"{first_name} {last_name}"
And that's it — no constructors to write, no validation, no manual checks.
The library knows that the Age constant holds an int, knows its default
value, and prepares an object with the matching interface.
A composition is then an object made up of constants and other objects, and its resulting value is derived from theirs.
That's the core principle of the whole library.
🏛️ Architecture at a glance
The whole system can be understood, in simplified form, as a series of layers:
┌────────────────────────────────┐
│ Definition classes │ ◄── SimpleBase, SimpleConstant, SimpleObject
└──────────────┬─────────────────┘ (what the user writes)
▼
┌────────────────────────────────┐
│ Metadata process │ ◄── SimpleMeta and *Metadata classes
└──────────────┬─────────────────┘ (validation and compilation at definition time)
▼
┌────────────────────────────────┐
│ Instance creation │ ◄── atom/composite × mutable/immutable, mixin injection
└──────────────┬─────────────────┘ (dynamically creates a new class and instance)
▼
┌────────────────────────────────┐
│ Mixins │ ◄── optional capabilities
└──────────────┬─────────────────┘ (extensibility without touching the core)
▼
┌────────────────────────────────┐
│ Tools & testing │ ◄── automate_creators, randomize, and testing utilities
└────────────────────────────────┘ (makes life easier for developers)
Each layer has its own responsibility and can be studied in more detail on its own.
🧩 Definition classes
SimpleBase
is the root of the hierarchy — purely architectural, not used directly.
SimpleConstant
defines an atom (_type, _default, optional _validate) and the public API
for validation and normalization.
SimpleObject
extends SimpleConstant with composition (_inners, _logic, optional
_decompose) — its _default is never written by hand; it's always derived
automatically from _logic(inners' defaults).
➡️ README_SIMPLE_BASE
➡️ README_SIMPLE_CONSTANT
➡️ README_SIMPLE_OBJECT
Defining constants
When you write, for example:
class Age(SimpleConstant):
_type = int
_default = 18
you no longer need to write a constructor by hand, store the value, check its type, or write a setter.
The library derives the necessary behavior from the definition.
age = Age()
age.value = 25
If you need to add a custom rule, you can simply do so:
class Age(SimpleConstant):
_type = int
_default = 18
@classmethod
def _validate(cls, value, *, return_bool=False):
if value < 0:
return cls.bool_or_raise_validate_error(
value,
"Age cannot be negative.",
return_bool=return_bool,
)
return True
This gives you an object that knows its type, its default value, and the rules for what counts as a valid value.
Composition and reactivity
The library's strength becomes much more apparent once objects start being composed of other objects.
SimpleObject defines:
_inners— what objects it's composed of,_logic— how the resulting value is built from them,_decompose— how the resulting value can optionally be decomposed back.
For example:
class FullName(SimpleObject):
_type = str
_inners = (FirstName, LastName)
@classmethod
def _logic(cls, first_name, last_name):
return f"{first_name} {last_name}"
A change to an inner value is automatically propagated to the composition:
person.first_name = "Peter"
# person.value == "Peter Smith"
And if the composition supports _decompose, the reverse direction also works:
┌───────────────┐
│ FullName │
│ "Peter Smith" │
└───────┬───────┘
│
_decompose
↙ ↘
┌──────────┐ ┌─────────┐
│ FirstName│ │LastName │
│ "Peter" │ │ "Smith" │
└──────────┘ └─────────┘
This makes the library build a bidirectionally reactive tree of values:
- changes inside propagate up toward the root,
- a composition's change can be decomposed down toward its parts.
An important property here is determinism — recalculation isn't based on a hidden event system or magic dependencies. The relationships between the parts are determined entirely by the object's own definition.
🧠 Metadata process
Behind the declarative layer sits the SimpleMeta metaclass and the metadata
system.
SimpleMeta
is the metaclass that governs a class's entire lifecycle — creation, write-once
protection, instance creation, and representation.
When a class is defined, its metadata is computed and permanently fixed:
SimpleBaseMetadata → SimpleConstantMetadata → SimpleObjectMetadata
A dry run is performed, along with validation of the provided attributes and
methods.
Every element is checked:
_type— whether it matches the default value,_default— whether it passes validation,_inners— whether they're composed ofSimpleObject/SimpleConstantclasses and don't contain conflicting names,_validate— whether it can correctly validate a value,_logic— whether it can compute its own default from the inners' default values,_decompose— whether it can compute the inners' default values from its own default.
This means definition errors can surface at class-creation time, rather than later when the object is actually used.
The metadata also serves as a fixed description of the blueprint, from which a concrete runtime instance is later prepared.
➡️ README_SIMPLE_META
➡️ README_SIMPLE_BASE_METADATA
➡️ README_SIMPLE_CONSTANT_METADATA
➡️ README_SIMPLE_OBJECT_METADATA
➡️ README_METHOD_PROCESSING
🧱 Instance creation
A blueprint is not itself an instance.
The actual runtime instance is created through dynamic compilation —
make_atom_class and make_composite_class assemble a concrete class with
slots, properties, and (for compositions) reactive hooks, and create_instance
safely attaches mixins to it via namespace injection. This avoids __slots__
conflicts while keeping a flat MRO regardless of how many mixins are used.
When an object is created, the library assembles a concrete runtime class based on the definition and prepares everything it needs:
- slots,
- properties,
- validation,
- reactive mechanisms,
- inner objects,
- any mixins.
The instance therefore contains only what it actually needs.
Thanks to the use of __slots__ and dynamic runtime-class assembly, there's no
need for a generic instance structure full of methods the given object will
never use.
➡️ README_ATOM_CLASS
➡️ README_COMPOSITE_CLASS
➡️ README_CREATE_INSTANCE
➡️ README_INSTANCE_PROTOCOLS
➡️ README_VALUE_PROPERTIES
🧩 Mixins
Mixins are what give instances their concrete capabilities — serialization, comparison, arithmetic, change history, an immutable update API, and much more.
The library ships with a rich set of ready-made mixins, and also offers the
SimpleMixin base class so you can write your own logic.
A mixin never inherits from SimpleObject directly — only from SimpleMixin.
When an instance is created, only the structural foundation (type, slots,
validation) is taken from the blueprint (SimpleObject/SimpleConstant), and
the mixin is layered on top of that.
A custom mixin therefore doesn't need to (and must not) know anything about the specific domain logic of the class it's applied to — it's a purely separate, reusable capability.
The library deliberately separates an object's structure from its capabilities:
SimpleObject says what an object is.
SimpleMixin says what an object can do.
This means the same structure can be used in different ways, depending on which capabilities you add to it.
Overview of available mixins
| Category | What they provide |
|---|---|
| Core | NodesTuple, NodesDicts, Snapshot, Infrastructure, Serialization |
| Collections | CollectionBase, Iterable, Mapping, Collection |
| Comparison | Equality, Ordering |
| Generics | ClassGetItem |
| Interfaces | Call, ContextManager |
| Lifecycles | Copyable, PickleState, PickleReduce, Lifecycle |
| Numeric | Arithmetic, Unary, Bitwise, Inplace, Conversion |
| Representation | ValueDisplay, ValueFormatting |
| Immutable | ImmutableMethods, ImmutableWithInners |
| Numerical | Volume, Equalizer |
| Properties | InnersProperty, MetaShortcuts |
| State | DefaultState, DirtyTracking, History, Permission |
A full description of each individual mixin (compatibility flags, slots, methods, what it builds on) is available in a dedicated overview:
🛠️ Tools & testing
The library isn't limited to manually defining classes by hand — the project also includes tools for:
-
automate_creators— Programmatically creating blueprints without writing a classicclassdefinition.
➡️ README_AUTOMATE_CREATORS -
bulk— Bulk creation of blueprints and instances from various inputs, such as JSON, YAML, CSV, or Python structures.
➡️ README_BULK -
randomize— Generating random values for objects and entire composition trees. Useful for fuzz testing or quickly generating test data.
➡️ README_RANDOMIZE -
testing— Testing utilities that let you verify whole blueprints or mixins without having to write dozens of individual tests by hand.
➡️ README_TESTING_OVERVIEW
➡️ README_BULK_TEST
➡️ README_MIXIN_TESTING
➡️ README_OBJECT_TESTING
⚠️ Exceptions
Every error the library raises inherits from a common root,
SimpleObjectError (built on top of simplibs-exception —
structured, readable diagnostic cards instead of a bare traceback).
This means you can catch any error from the library with a single
except SimpleObjectError, or target just one specific category:
| Exception | When it happens |
|---|---|
SimpleDefinitionError |
An error in a class definition — a malformed _inners, a disallowed _default on a composition, an invalid _logic signature. Raised at class import/definition time. |
SimpleInitializationError |
An error during instance creation — invalid mixins/mutable parameters, an incompatible mixin, failure to assemble inner elements. |
SimpleRuntimeError |
An error in data or at runtime — an invalid value on write, a failed _validate, an attempt to write to an immutable instance. This is the exception you'll run into most often during normal use. |
from simplibs.object.exceptions import SimpleObjectError
try:
age.value = -5
except SimpleObjectError as e:
print(e) # a structured diagnostic card: what, why, how to fix it
SimpleObjectError also filters the library's own internal frames out of the
error's traceback (_skip_locations) — the error message points to your
code, not the library's internal implementation.
⚙️ Settings
The library has one configurable point: logging. There's no automatic handler setup and no interference with the root logger — everything is opt-in and fully under your control.
import logging
logging.getLogger("simple").setLevel(logging.DEBUG)
You can also register a custom callback that runs on every value change
(value = ... at the root level):
import simplibs.object.settings.logging as simple_logging
def my_callback(cls, value, event):
print(f"{cls.__name__}: {event} -> {value}")
simple_logging.on_value_change = my_callback
🧱 Benefits of the library
Every object has its own type and rules for working with its value.
Depending on the definition, it can have:
- a default value,
- validation,
- normalization,
- composition,
- automatic recalculation,
- mutable or immutable behavior,
- custom mixins.
This means the same foundation can be used for very simple values as well as for more complex data models.
Declarative data models
An object's structure can be described with a handful of class attributes instead of a pile of repetitive boilerplate.
Validation and normalization
Type checking, default values, and custom validation rules are part of the object's own definition.
Reactive calculations
When one part of the model affects another, the change automatically propagates to the corresponding part of the tree.
This can be useful for things like:
- configuration,
- calculations,
- simulations,
- state models,
- forms,
- API data,
- or your own domain models.
Immutability
An immutable instance has no setter for changing its value.
Instead of modifying an existing instance, you can create a new one via the immutable API:
new_age = age.with_value(25)
The original instance remains untouched.
🔭 About the library, from the author's point of view
The library grew out of a simple observation — a recurring pattern of "type + default + validation + logic" — and a wish to give it a deterministic, reusable foundation once and for all. It's a bit like a small language of its own for talking to Python: minimal logic, maximum usefulness. Every function parameter becomes its own object, and together they form a reactive system.
I plan to use it myself as the foundation for the upcoming simplibs-validate
— validation logic, where a SimpleConstant with its own _validate is
exactly the right building block. But I see the potential as broader than
that: since any constant can be turned into a composition by adding _inners,
and any composition can conversely be simplified into a constant (by removing
_inners, or by creating an instance with as_atom=True), the library can
also be used as a general declarative building system — extensible and
specifiable in both directions, starting from the smallest units and building
up. One idea I'd like to try out at some point: using constants as elementary
building blocks and composing descriptions of atoms and molecules out of them
— literally, not in the metaphorical programming sense.
This is the first version — the result of all the original ideas, but definitely not a finished work. It's more of a solid foundation and structure that's a pleasure to keep building on, than a finished product. Real-world use will show, over time, what still needs tuning, extending, or rethinking entirely.
☯️ About simplibs
All libraries in the simplibs (Simple Libraries) ecosystem share a common engineering philosophy:
- Dyslexia-friendly: We actively minimize cognitive load. Code is atomized into small, self-contained units, files are named directly after the job they do, and explanations focus more on why something is designed the way it is than just what it does.
- Programmer's peace of mind: Nothing should be missing, and nothing should be redundant. We value clean execution paths and understandable architecture over a rushed, disorganized pile of features.
- Defensive style: We actively anticipate edge cases and error states, so that only safe execution paths remain. Code is built to degrade gracefully, not to crash unexpectedly.
- Minimalism: Find the most direct path to the goal in as few steps as possible, without compromising on safety, readability, or completeness.
- Code as craft: Code should be pleasant to look at, readable at a glance, and evoke structural harmony. We treat software engineering as a precise craft.
🤝 Contributing and community
This is an open-source project, made with care. We deeply value collaboration with the community and welcome any feedback, bug reports, or ideas for new features!
- Want to contribute? Feel free to open an Issue or send a Pull Request.
- Want to reach out? If you'd like to discuss the project further, collaborate, or just say hi, open a GitHub Issue or start a Discussion.
📝 License
This library is released under the MIT license. Build great things!
Release files for simplibs-object 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| simplibs_object-0.1.0.tar.gz | 347.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| simplibs_object-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.0 MB
Release files / simplibs_object-0.1.0.tar.gz
| Download URL | simplibs_object-0.1.0.tar.gz |
|---|---|
| Size | 347.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1b32827a188b54df6accadf5ee098aa519631bb2eda86b6514810e6ab52dcdab
|
|
BLAKE2b-256 checksum How to use checksums |
e1b3ca9eac31c728c0c0436a35ff0ab2c42ac803a6bf9315b1e3d170ba6d6561
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / simplibs_object-0.1.0-py3-none-any.whl
| Download URL | simplibs_object-0.1.0-py3-none-any.whl |
|---|---|
| Size | 673.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a50806ac1be38676c13a1b4e5b4b22b68f6e8c546d69461213aeae09ea2b9a32
|
|
BLAKE2b-256 checksum How to use checksums |
26d1109f7b514c42b0e15ddc387f5badb7a5d8ec0e475a11a810a2f61466dea6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|