django-fsm

django-fsm defines states and the methods that move an object between them. It provides model fields for Django applications and a State descriptor for plain Python objects. This section documents the released django-fsm package and its django_fsm imports.

Installation

For Django model fields:

python -m pip install 'django-fsm[django]'

For plain Python state machines, install django-fsm without extras. Python 3.10+ is required. Model fields support Django 5.2+; Django 6.0+ requires Python 3.12+.

Models and plain Python objects are two ways to use the same transition engine. A separate flow class is optional. The Django Model State Fields page covers model-backed states; Separate Flow Classes shows how to wrap an existing model field.

Django Model Quick Start

Declare a field and transitions on the model:

from django.db import models
from django_fsm import FSMField, can_proceed, transition

class Report(models.Model):
    status = FSMField(default='draft', protected=True)

    @transition(field=status, source='draft', target='published')
    def publish(self):
        pass

report = Report.objects.create()
if can_proceed(report.publish):
    report.publish()
    report.save()

Transitions update the instance in memory. Call save() to persist the state. protected=True blocks direct reassignment after initialization. See Django Model State Fields for integer and foreign-key states, signals, error targets, proxy models and concurrent state checks.

Plain Python Quick Start

State stores state on an ordinary Python object. Django and its settings are not required:

from enum import Enum
from django_fsm import State

class Stage(Enum):
    DRAFT = 'draft'
    PUBLISHED = 'published'

class Review:
    state = State(Stage, default=Stage.DRAFT)

    @state.transition(source=Stage.DRAFT, target=Stage.PUBLISHED)
    def publish(self):
        return self.state

review = Review()
assert review.publish() == Stage.PUBLISHED

Direct assignment to review.state raises AttributeError. An ordinary target is applied before the method runs; a second publish() call is refused because the source no longer matches. See Transition Options for source wildcards, conditions, permissions and dynamic targets.

Handling Refused Transitions

TransitionNotAllowed is the base exception for transition failures. NoTransition means the current state has no matching transition; TransitionConditionsUnmet means a condition rejected it.

from django_fsm import NoTransition, TransitionConditionsUnmet

try:
    review.publish()
except TransitionConditionsUnmet as exc:
    print(exc.failed_condition, exc.unmet_message)
except NoTransition:
    print('No transition from the current state')

A direct method call checks the source and conditions, not user permissions. Use has_transition_perm(method, user) for model transitions or method.has_perm(user) for the native API before accepting a user action.

Common Questions

Do I need Django for a state machine?

No. State, forward references, dynamic targets and DOT charts work on plain Python objects. Install the Django extra when using model fields.

Do I need a separate flow class for a model?

No. FSMField and @transition(field=...) put state and transitions on the model. A separate State flow class is useful when the model already has a state column or the transition logic should live elsewhere.

When is the target state applied?

An ordinary target and GET_STATE apply before the method body runs. RETURN_VALUE applies after the method returns. Failure restores state in memory or applies on_error on model fields; database writes need a transaction to roll back together.

Documentation

The admin and frontend viewset chapters describe optional Viewflow Pro integrations. REST integration uses Django REST Framework. These integrations are separate from the core state machine API.

For processes with parallel branches and multiple participants, see the BPMN workflow engine.