Skip to main content

PyPI version Python versions CI License: MIT Downloads Ruff Checked with pyright

sqlalchemy-fsm

Declarative finite state machine for SQLAlchemy models. Add an FSMField column, decorate methods with @transition, and let the library enforce which transitions are reachable from which states.

Requirements

Python 3.10+, SQLAlchemy 1.4+ (2.x supported).

Install

pip install sqlalchemy-fsm

Quickstart

import sqlalchemy as sa
from sqlalchemy.orm import declarative_base
from sqlalchemy_fsm import FSMField, transition

Base = declarative_base()


class BlogPost(Base):
    __tablename__ = "blog_post"
    id = sa.Column(sa.Integer, primary_key=True)
    state = sa.Column(FSMField, nullable=False, default="draft")

    @transition(source="draft", target="published")
    def publish(self):
        """Side effects of publishing go here (notifications, cache busts, …).
        The return value is discarded."""

    @transition(source=["draft", "published"], target="archived")
    def archive(self):
        ...


post = BlogPost()
post.publish.can_proceed()   # True — we're in 'draft'
post.publish.set()           # state is now 'published'
post.publish.set()           # raises InvalidSourceStateError

source accepts a single state, a list of states, "*" (any state), or None (matches a nullable column's NULL).

Transition API

For a transition decorated as BlogPost.publish:

Expression Returns
BlogPost.publish() SQLAlchemy filter for rows in the transition's target state — use in .filter(...).
BlogPost.publish.is_(True) Equivalent to BlogPost.publish() == True.
post.publish() bool — is this instance currently in the target state?
post.publish.set(*args, **kwargs) Execute the transition. Raises InvalidSourceStateError if the current state isn't allowed, or PreconditionError if any condition returns falsy.
post.publish.can_proceed(*args, **kwargs) bool — would set() succeed right now?

set() mutates the field in memory; commit the session yourself to persist.

Note: target=None is not supported — every transition must declare an explicit target state.

Conditions

Pass callables to conditions to gate the transition. Each is called with the instance (plus any args forwarded from set() / can_proceed()) and must return truthy.

def can_publish(instance) -> bool:
    return datetime.now().hour <= 17

class BlogPost(Base):
    ...
    @transition(source="draft", target="published", conditions=[can_publish])
    def publish(self):
        ...

# can_proceed() must receive the same args you'd pass to set():
post.publish.can_proceed()   # checks conditions without mutating
post.publish.set()

Conditions must be side-effect-free — can_proceed() evaluates them too.

Class-grouped transitions

To branch on the source state with different handlers, decorate a class:

@transition(target="published")
class publish:
    @transition(source="draft")
    def from_draft(self, instance):
        instance.published_via = "fresh"

    @transition(source="archived")
    def from_archive(self, instance):
        instance.published_via = "republish"

Invocation is still post.publish.set() — the right sub-handler is picked by the current state.

Query helpers

Use the class-bound form inside .filter():

session.query(BlogPost).filter(BlogPost.publish())          # currently 'published'
session.query(BlogPost).filter(~BlogPost.publish())         # everything else

Events

The library hooks into SQLAlchemy's event system and emits before_state_change and after_state_change per transition:

from sqlalchemy.event import listens_for

@listens_for(BlogPost, "after_state_change")
def on_change(instance, source, target):
    ...

Remove with sqlalchemy.event.remove(...).

Type checking

The package ships type information (PEP 561 py.typed). pyright / mypy pick up annotations automatically once installed.

Development

pdm install                                 # project + dev deps
pdm run pytest                              # tests
pdm run ruff check ./src ./tests            # lint
pdm run ruff format --check ./src ./tests   # format check
pdm run pyright                             # type check

Releasing

Tagged commits drive releases:

git tag v2.1.0
git push --follow-tags

CI runs the matrix, pdm-backend derives the version from the tag, the artifacts are Sigstore-signed and published to TestPyPI then PyPI via OIDC trusted publishing. A GitHub Release is created with notes from CHANGELOG.md.

How does this differ from django-fsm?

  • Cannot commit data from inside a transition handler.
  • Condition callables accept arguments forwarded from set().

Metadata

Release files for sqlalchemy-fsm 2.2.0

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

Source distribution (sdist)

Source distribution for sqlalchemy-fsm 2.2.0
File Size Uploaded Provenance Actions
sqlalchemy_fsm-2.2.0.tar.gz 23.2 kB With provenance Details for sqlalchemy_fsm-2.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for sqlalchemy-fsm 2.2.0
File Interpreter ABI Platform Actions
sqlalchemy_fsm-2.2.0-py3-none-any.whl Python 3 none any Details for sqlalchemy_fsm-2.2.0-py3-none-any.whl

Total release size: 37.8 kB

Release files / sqlalchemy_fsm-2.2.0.tar.gz

Download URL sqlalchemy_fsm-2.2.0.tar.gz
Size 23.2 kB
Tags Source
SHA-256 checksum
How to use checksums
0dbbafd8c5ff517a51c9f5a4d244bdfe93b5c5c476acb6d4e4aa90dc3da450f6
BLAKE2b-256 checksum
How to use checksums
48c18a03a0bb3e75ed06486af6218c13cf9204073b57d78d6777b4a52011f222
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jun 8, 2026.

Transparency log

Release files / sqlalchemy_fsm-2.2.0-py3-none-any.whl

Download URL sqlalchemy_fsm-2.2.0-py3-none-any.whl
Size 14.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0d2c158084e979c1841099fb84b9b5c38df13d3803edcd84378c4f828958aa69
BLAKE2b-256 checksum
How to use checksums
d540049e07557f97bad8878851d43c5c5c72d5bfecb3016f6872b1eec29c9036
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jun 8, 2026.

Transparency log

Security for sqlalchemy-fsm 2.2.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.

Select from 2 files

Attestation for sqlalchemy_fsm-2.2.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 Jun 8, 2026.

Transparency log

A 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.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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