Getting Started
Step 1: Installation
Namespace
The package is published as iron-monk but imported as monk. Always import monk (or from monk import ...) in your code.
Step 2: Annotate Your Types
iron-monk provides the same toolkit across every Python surface — classes, functions, methods, raw dicts, and even standalone calls.
from typing import Annotated
from monk import monk
from monk.constraints import Email, Interval
@monk # (1)!
class User:
email: Annotated[str, Email]
age: Annotated[int, Interval(ge=18)]
- The
@monkdecorator on aclassconverts it to adataclassand locks attribute access until validation runs (see Step 3).
from typing import Annotated
from monk import monk
from monk.constraints import Email, Interval
@monk # (1)!
def process_user(
email: Annotated[str, Email],
age: Annotated[int, Interval(ge=18)],
) -> None:
...
- The
@monkdecorator on a function wraps it; arguments are validated immediately on every call.
from typing import Annotated
from monk import monk
from monk.constraints import Email
class NotificationService:
@classmethod # (1)!
@monk # (2)!
def broadcast(cls, email: Annotated[str, Email]) -> None:
...
@classmethodis shown here, but@staticmethodand instance methods work the same way. Always place@monkbelow the binding decorator.- Arguments are validated immediately on every call, exactly like a free function.
from typing import Annotated, TypedDict
from monk.constraints import Email, Interval
class UserDict(TypedDict): # (1)!
email: Annotated[str, Email]
age: Annotated[int, Interval(ge=18)]
- No decorator required —
iron-monkreads constraints directly off theTypedDict. A@monkdataclass also works as a schema target.
validate_value(value, *constraints)runs one or more constraints against a single value. No class needed — useful for controllers, scripts, and one-shot checks (see Step 3). Every constraint also exposes a raw.validate(value)method for direct use.
Step 3: Validate
iron-monk aggregates every field-level error into a single ValidationError. It does not fail-fast — pass an object with two bad fields and you get two errors back, not one.
ValidationError.errors is a list[ErrorDict] of {field, message, code}. Two helpers are available on every error:
.flatten()— flat["field: message", ...]strings..to_rfc7807()— RFC 7807 problem-detail dict for HTTP APIs.
from monk import validate
from monk.exceptions import ValidationError
user = User(email="bad-email", age=12) # (1)!
try:
validate(user) # (2)!
except ValidationError as e:
print(len(e.errors)) # 2 (3)!
# Happy path
good = validate(User(email="kai@example.com", age=30))
print(good.email) # (4)!
- Instantiation is instant. By default,
iron-monkdefers validation until you callvalidate()— attribute access on an unvalidated instance raisesUnvalidatedAccessError. Use@monk(defer=False)for fail-on-construct semantics. - Both
emailandageare invalid; both errors are collected. - Two errors, not one —
iron-monknever fails fast. - After successful validation, attribute access is unlocked.
from monk.exceptions import ValidationError
try:
process_user(email="bad-email", age=12) # (1)!
except ValidationError as e:
print(len(e.errors)) # 2
process_user(email="kai@example.com", age=30) # (2)!
- Function arguments are validated eagerly on call.
- Valid input — no exception, function runs normally.
from monk.exceptions import ValidationError
try:
NotificationService.broadcast(email="bad-email")
except ValidationError as e:
print(e.flatten()) # ['email: Must be a valid email address.']
NotificationService.broadcast(email="kai@example.com") # (1)!
- Methods validate exactly like free functions.
from monk import validate_dict
from monk.exceptions import ValidationError
try:
validate_dict({"email": "bad-email", "age": 12}, UserDict) # (1)!
except ValidationError as e:
print(len(e.errors)) # 2
validate_dict({"email": "kai@example.com", "age": 30}, UserDict) # (2)!
- No object instantiation —
iron-monkwalks the raw dict against the schema. - Returns the validated dict on success. Pass
partial=Truefor PATCH-style payloads ordrop_extra_keys=Trueto strip unknown keys.
from monk import validate_value
from monk.constraints import Email, Len
from monk.exceptions import ValidationError
try:
validate_value("X", Len(min_len=3), Email) # (1)!
except ValidationError as e:
print(len(e.errors)) # 2 (2)!
validate_value("kai@example.com", Email) # (3)!
# Lower-level: skip aggregation, raise native ValueError/TypeError
Email().validate("bad-email") # (4)!
validate_valueaggregates every per-constraint failure into a singleValidationError(same semantics asvalidate()). Usefield_name="..."to customize the field label ine.flatten()output.- Both
LenandEmailfail — both errors collected, no fail-fast. - Happy path. Returns
None; raises only on failure. - For raw single-constraint checks where aggregation is not needed, call the constraint's
.validate()directly — it raises nativeValueError/TypeError.
Next Steps
- Core Concepts — the validation lifecycle, defer semantics, and error model.
- Constraints Toolkit — the full catalog of built-in constraints.
- Advanced Usage — cross-field rules, custom constraints, and framework integration.