Skip to main content

🧬 simplibs-object

PyPI Python Licence

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 of SimpleObject/SimpleConstant classes 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.

➡️ README_SIMPLE_MIXIN


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:

➡️ README_MIXINS_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 classic class definition.
    ➡️ 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!


▲ Back to top

Release files for simplibs-object 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for simplibs-object 0.1.1
File Size Uploaded
simplibs_object-0.1.1.tar.gz 347.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for simplibs-object 0.1.1
File Interpreter ABI Platform
simplibs_object-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 1.0 MB

Release files / simplibs_object-0.1.1.tar.gz

Download URL simplibs_object-0.1.1.tar.gz
Size 347.2 kB
Tags Source
SHA-256 checksum
How to use checksums
1b54a2a931dafad446c9cecc6917aeaf2741d419df08b1378a51a83f96106e89
BLAKE2b-256 checksum
How to use checksums
d097d52ba3a7b52a0025fac9fbe6cafc6255b6f4852b076b5a26338254e6fcba
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.1-py3-none-any.whl

Download URL simplibs_object-0.1.1-py3-none-any.whl
Size 673.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ede23071d4e7918b8e5470a15d474c2fcccff306b60dd9fe83aa4543fa3d3c26
BLAKE2b-256 checksum
How to use checksums
9fcbf1a03c678003b9a5d471bb9c3798bbe2f1bb64eb9074602704a644627406
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page