⚖️ simplibs-validate
The validation layer on top of simplibs-rules — entry points, ready-made validators, and self-validating decorators.
simplibs-rules defines the atomic Rule objects and lets you compose them with
|/&/~. simplibs-validate is what turns a rule (or a whole function signature)
into an actual validation action: a single validate() call, a batteries-included
validate_* wrapper for common types, or a decorator that validates a function or
dataclass automatically from its own type hints.
from simplibs.validate import validate
from simplibs.rules import is_integer, greater_than
validate(5, is_integer & greater_than(0)) # -> True
validate(-5, is_integer & greater_than(0)) # -> raises ValidationError
🧭 The Core Philosophy
simplibs-validate doesn't define new predicates of its own — that's
simplibs-rules's job. What it adds is the
layer people actually reach for day to day: a universal validate() entry point,
ready-made validate_* functions for common types so you rarely need to hand-compose a
rule tree, and decorators that make an entire function or dataclass self-validating from
nothing more than its own type annotations:
from simplibs.validate import validate_call
from simplibs.types import validated_type
from simplibs.rules import greater_than
PositiveInt = validated_type(int, greater_than(0))
@validate_call
def register(age: PositiveInt, *, validate: bool = True) -> None:
...
register(25) # validated normally
register(-5) # raises ValidationError
register(-5, validate=False) # explicitly skipped — e.g. already validated upstream
That combination — annotation-driven rules from simplibs-rules, named reusable types
from simplibs-types, and a decorator that enforces them automatically with a per-call
opt-out for code paths that already trust their data — is what lets you build fully
self-validating functions and dataclasses from nothing more than their own signatures.
📦 Installation
pip install simplibs-validate
simplibs-rules is installed automatically as a dependency — its rules and operators
(is_integer, greater_than, IsTyping, ...) are what you pass into everything below.
🚀 Quick Start in 60 Seconds
Level 1: One-off validation
from simplibs.validate import validate
from simplibs.rules import is_integer, greater_than
validate(5, is_integer & greater_than(0))
validate(5, is_integer & greater_than(0), return_bool=True) # -> True, no exception
validate("x", is_integer, return_bool=True) # -> False, no exception
Level 2: Ready-made validators
Every common type has a batteries-included validate_* function, taking the
constraint as plain keyword arguments — no rule composition required:
from simplibs.validate import validate_string, validate_int
validate_string("user@example.com", contains="@", min_length=5)
validate_int(42, greater_than=0, divisible_by=2)
Level 3: Annotation-driven, self-validating functions
from simplibs.validate import validate_call
@validate_call
def create_user(name: str, age: int) -> dict:
return {"name": name, "age": age}
create_user("Alice", 30) # validated automatically from the annotations
create_user("Alice", "30") # raises ValidationError
🛠️ The Architecture: 3 Layers
┌──────────────────────────┐
│ simplibs-rules │ ◄── Rule subclasses + snake_case shortcuts
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ validators │ ◄── validate, raise_invalid, validate_string, validate_int, ...
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ decorators & tools │ ◄── validate_call, validate_dataclass, log_this, ...
└──────────────────────────┘
1. validate — the universal entry point
Every validation ultimately goes through one of two functions:
validate(value, rule, ...)— evaluatesruleagainstvalue, then either returns (True/the value) or raises, depending on the flags given. Use this everywhere you actually need the check performed.raise_invalid(value, rule, ...)— unconditionally builds and raises the diagnostic exception forrule, without evaluating anything. Use this where your own code has already determined a value is invalid (e.g. inside anif not condition:branch) and you just want the same structuredValidateErrorcardvalidate()would have produced, without redundantly re-running the check.
def validate(
value: Any,
rule: Rule | Callable[[Any], bool],
*,
value_name: str | None = None,
context: str | None = None,
return_bool: bool = False,
return_value: bool = False,
) -> Any:
# 1. Rule instance handling — delegate entirely to Rule.validate()
if isinstance(rule, Rule):
return rule.validate(
value,
value_name=value_name,
context=context,
return_bool=return_bool,
return_value=return_value,
)
# 2. Callable handling (plain function / lambda)
# 2.1 Validation execution and success handling
if rule(value):
return value if return_value else True
# 2.2 Return bool handling
if return_bool:
return False
# 2.3 Failure handling
raise build_validation_error(
rule,
value,
value_name=value_name,
context=context,
)
def raise_invalid(
value: Any,
rule: Rule | Callable[[Any], bool],
*,
value_name: str | None = None,
context: str | None = None,
) -> NoReturn:
# 1. Rule instance handling
if isinstance(rule, Rule):
raise rule.build_exception(
value,
value_name=value_name,
context=context,
)
# 2. Callable handling (plain function / lambda)
raise build_validation_error(
rule,
value,
value_name=value_name,
context=context,
)
Both accept either a Rule instance or a plain callable predicate — a Rule delegates
to its own validate()/build_exception(), while a callable is evaluated directly and,
on failure, wrapped in a generic diagnostic via build_validation_error.
2. Specialized validators
For the most common types, a ready-made validate_* function exposes every relevant
constraint as a plain keyword argument, composing the equivalent Rule tree
internally — no manual &-chaining required for everyday cases.
| Validator | Description | Docs |
|---|---|---|
validate_bool |
Boolean, optionally against an exact expected value. | README_VALIDATE_BOOL |
validate_container |
Any non-string container — length, uniqueness, membership, subset/superset, per-item rule. | README_VALIDATE_CONTAINER |
validate_float |
Float — comparisons, range, approximate equality, finiteness. | README_VALIDATE_FLOAT |
validate_int |
Integer — comparisons, range, divisibility, remainder. | README_VALIDATE_INT |
validate_mapping |
dict — length, single/multiple key membership. |
README_VALIDATE_MAPPING |
validate_number |
Any number (int/float/Decimal/complex) — comparisons, range, membership. |
README_VALIDATE_NUMBER |
validate_string |
String — length, prefix/suffix/substring, regex, blankness, membership. | README_VALIDATE_STRING |
validate_type |
Class/type object — subclass, identity, membership. | README_VALIDATE_TYPE |
Each validate_* is a thin wrapper: it composes its matching *_rule(...) factory and
calls .validate() on the result. For repeated validation against the same
constraints, build the rule once with *_rule(...) and reuse it, instead of calling
validate_* inside a loop.
🧰 Tools
Beyond individual rules, the decorators and tools packages provide the
convenience layers that make validation part of a function's or dataclass's
definition:
🎀 Decorators
The decorators package provides decorators that add validation or structured
logging directly to functions and dataclasses:
validate_call— validates a function's arguments (and optionally its return value) against its own type annotations, on every call. Supports selective validation (check), extra constraints (overrides), and a per-call bypass switch.validate_dataclass— the@dataclasscounterpart: validates every field against its annotation on instance construction, before any field is assigned.log_this— gives any function entry/exit/timing/exception logging, entirely independent of validation, without imposing any logging configuration of its own.
🛠️ Tools
The tools package provides small helpers used to build reusable validation
annotations and the rule mappings consumed by the decorators:
validated_type— names a reusableAnnotated[type, rule(s)]combination once, for use across multiple annotations.override_rules— batch-builds theoverrides=mappingvalidate_call/validate_dataclassexpect, from keyword arguments.
➡️ README_TOOLS
⚠️ Exceptions
Every exception raised by simplibs-validate is built on top of
simplibs.exception.SimpleException —
structured, readable diagnostic cards instead of a bare traceback.
The library's single common root is ValidateError:
class ValidateError(SimpleException):
"""Root exception class for all errors originating from simplibs-validate."""
skip_locations = ("simplibs/validate",)
Two concrete subclasses distinguish what kind of mistake occurred:
| Exception | When it happens |
|---|---|
ParamError |
A developer error made while constructing a rule or configuring a decorator — e.g. HasLength() with no length source, validate_call(check=("x",)) where x has no annotation or override. |
ValidationError |
An invalid runtime value — the value being checked simply doesn't satisfy the rule. This is the exception you'll encounter during ordinary, everyday use. |
try:
validate(-5, greater_than(0))
except ValidateError as e:
print(e) # a structured diagnostic card: what, why, how to fix it
Catching ValidateError catches both categories at once; catching ValidationError
or ParamError specifically lets you distinguish "bad input data" from "the
validation itself was set up incorrectly." ValidateError.skip_locations also filters
the library's own internal frames out of the error's reported location — the message
points to your code, not this library's implementation.
🧪 Testing Utilities
simplibs-validate ships with testing infrastructure for the layer it owns — the
wrappers, decorators, and validated types, not the underlying atomic rules (see simplibs-rules'
assert_rule_contract for that):
assert_validate_wrapper— verifies avalidate_*convenience function correctly wraps its underlying*_rulefactory and delegates properly toRule.validate()— signature alignment, successful/failed delegation, both return modes.assert_type_contract— master orchestrator that verifies avalidated_type()-built construct against the fullRulecontract battery (by decomposing it viabuild_typing_rule) and optionally tests its execution under@validate_call.assert_type_validate_call_integration— specialized integration probe verifying that a custom type annotation is correctly intercepted and enforced when used on parameters of a@validate_call-decorated function.
➡️ README_TESTING_ASSERTS_VALIDATE_WRAPPER
➡️ README_ASSERT_TYPE_CONTRACT
➡️ README_ASSERT_TYPE_VALIDATE_CALL_INTEGRATION
🔭 About the library, from the author's point of view
simplibs-validate used to bundle the rule engine itself; that engine has since moved
to its own library, simplibs-rules, so
that the atomic predicates can be depended on independently of the higher-level
validation entry points and decorators defined here. What remains here is deliberately
focused: validate(), the validate_* convenience layer, and the
validate_call/validate_dataclass decorators — with room to grow as real-world use
shows which additional wrappers or tools are worth adding.
🔗 Related libraries
simplibs-rules— theRulebase class, operator composition, and every built-in predicate (is_integer,greater_than,IsTyping, ...) used throughout this library.simplibs-types(in progress) — reusable, named validated types (validated_typeand friends) built onsimplibs-rules, for sharing a single constraint definition across manyvalidate_call/validate_dataclassannotations.
☯️ 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 logical task they perform, and explanations describe why something is designed, not just what it is.
- Programmer's Zen: Nothing should be missing, and nothing should be superfluous. We value clean execution paths and robust, understandable code architectures over rushed, messy feature sets.
- Defensive Style: We actively anticipate edge cases and failure modes so that only safe operational paths remain. Our code is built to degrade gracefully rather than crash unexpectedly.
- Minimalism: Find the most direct path to the goal in as few operational steps as possible without taking shortcuts 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 precision trade.
🤝 Contributing & Community
This is an open-source project built with love and care. We strongly believe in community collaboration and welcome any feedback, bug reports, or feature ideas!
- Want to contribute? Feel free to open an Issue or submit a Pull Request.
- Want to get in touch? If you'd like to discuss the project further, collaborate, or just say hello, feel free to open a GitHub Issue or start a Discussion.
📝 License
This library is released under the MIT License. Build great things!
Release files for simplibs-validate 0.2.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_validate-0.2.0.tar.gz | 70.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| simplibs_validate-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 168.1 kB
Release files / simplibs_validate-0.2.0.tar.gz
| Download URL | simplibs_validate-0.2.0.tar.gz |
|---|---|
| Size | 70.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1497b4021a408260ae4cb80cef6838863071621376c8206e9476b0056a6f24f2
|
|
BLAKE2b-256 checksum How to use checksums |
b18c3d02730ca0f2a36367bbad9ac5db33c06e5cddd0e0c105369a063137c28f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / simplibs_validate-0.2.0-py3-none-any.whl
| Download URL | simplibs_validate-0.2.0-py3-none-any.whl |
|---|---|
| Size | 97.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1f34e3903e8b39abae626c21ac417169e9fb978c85d2d56dc3cf571d33539d96
|
|
BLAKE2b-256 checksum How to use checksums |
d7904bc8cf5618532e86e5fbfe33dbc6b264f64b27a662b6cb3ccf6d71e9da52
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|