invariants
Type-safe state modeling for Python. Split entity invariants into separate, validated, immutable state classes using Pydantic.
Why?
When an entity has multiple states, each state often has different field requirements. Typically, this is modeled with optional fields and runtime checks. invariants enforces these constraints at the type level:
- Each state is a separate class with its own field types
- A metaclass ensures child states override all abstract fields and don't add new ones
- All states are immutable
StateMachinedefines valid transitions with type-safe signatures
Installation
pip install invariants
Quick Start
Define a root state with Statefull fields (abstract markers), then create concrete states that override them:
from datetime import datetime
from typing import Literal
from invariants.state import State, Statefull, StateMachine
class LoanState(State):
id: int
status: Statefull
postponement_date: Statefull
class ActiveLoan(LoanState):
status: Literal["active"] = "active"
postponement_date: datetime
class ClosedLoan(LoanState):
status: Literal["closed"] = "closed"
postponement_date: None = None
class OverdueLoan(LoanState):
status: Literal["overdue"] = "overdue"
postponement_date: datetime
The metaclass enforces two rules at class creation time:
- Child states cannot add new fields — only override parent fields
- Child states must override all
Statefullfields with concrete types
Define state transitions with StateMachine:
class CloseLoan(StateMachine[LoanState]):
def execute(self, loan: ActiveLoan | OverdueLoan) -> ClosedLoan:
return ClosedLoan(id=loan.id)
class MakeLoanOverdue(StateMachine[LoanState]):
def execute(self, loan: ActiveLoan) -> OverdueLoan:
return OverdueLoan(id=loan.id, postponement_date=loan.postponement_date)
The execute method signature documents which transitions are valid — from which states to which.
Nested States
States can contain other states. Use ContainsOne to validate that a collection includes at least one instance of a specific type:
from typing import Annotated, Literal
from invariants.conditions import ContainsOne
class DebtState(State):
status: Statefull
loans: Statefull
class ActiveDebt(DebtState):
status: Literal["active"] = "active"
loans: Annotated[tuple[ActiveLoan | ClosedLoan, ...], ContainsOne(ActiveLoan)]
class ClosedDebt(DebtState):
status: Literal["closed"] = "closed"
loans: tuple[ClosedLoan, ...]
class OverdueDebt(DebtState):
status: Literal["overdue"] = "overdue"
loans: Annotated[
tuple[OverdueLoan | ActiveLoan | ClosedLoan, ...], ContainsOne(OverdueLoan)
]
ActiveDebt guarantees at compile time that it only contains active or closed loans, and at runtime that at least one is active.
Factories (Testing)
Optional integration with polyfactory for generating test data. The key idea: a factory always produces a valid state — all business invariants are satisfied automatically.
pip install invariants[factories]
Define factories for concrete states — the model is inferred from the generic argument:
from invariants.factories import StateFactory
class ActiveLoanFactory(StateFactory[ActiveLoan]): ...
class ClosedLoanFactory(StateFactory[ClosedLoan]): ...
class ActiveDebtFactory(StateFactory[ActiveDebt]): ...
When you build a nested state, child entities are generated automatically:
debt = ActiveDebtFactory.build()
# loans are auto-populated with valid ActiveLoan | ClosedLoan instances
# ContainsOne(ActiveLoan) is always satisfied — at least one loan is active
assert any(isinstance(loan, ActiveLoan) for loan in debt.loans)
SQLAlchemy Integration
SQLAlchemyStateFactory maps Pydantic states to ORM models — build() returns ORM instances instead of Pydantic models:
from invariants.factories.sqlalchemy import SQLAlchemyStateFactory
class ActiveLoanORMFactory(SQLAlchemyStateFactory[ActiveLoan, LoanORM]): ...
loan = ActiveLoanORMFactory.build()
assert isinstance(loan, LoanORM) # ORM instance, not Pydantic model
Both StateFactory and SQLAlchemyStateFactory are thin wrappers over polyfactory — all interfaces (sessions, persistence, batch creation) work the same as in the polyfactory docs.
XState Visualization
Generate XState v5 state machine code from your StateMachine subclasses:
# Print generated JS — paste it into https://sketch.stately.ai
python -m invariants.viz print examples.debt
# Start a local Stately Sketch with one document per state machine
# (clones https://github.com/statelyai/sketch on first run; needs Node.js with pnpm or npx)
python -m invariants.viz serve examples.debt
serve opens each machine in the browser
Metadata
Release files for invariants 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 | Provenance | |
|---|---|---|---|---|
| invariants-0.1.0.tar.gz | 35.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| invariants-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 57.2 kB
Release files / invariants-0.1.0.tar.gz
| Download URL | invariants-0.1.0.tar.gz |
|---|---|
| Size | 35.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
872cbd3cb26b8eebdd279cbf451bbb28c02dd844114db9c78cc02a440f36e047
|
|
BLAKE2b-256 checksum How to use checksums |
7964dfad9e4e10685ef0c53844ebe52ce80a1bb8c05ad594b031ebcfade08961
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / invariants-0.1.0-py3-none-any.whl
| Download URL | invariants-0.1.0-py3-none-any.whl |
|---|---|
| Size | 22.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
64cf7ecc0711e26dc1c7baca8ce0d5aa75b473309823f878b24e98268a30107a
|
|
BLAKE2b-256 checksum How to use checksums |
74cc5c85bdaaf84ee3fa3905af398d98292ffb834e5465741be05a8baaeea747
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for invariants 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for invariants-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.