Skip to content

Migrate from factory_boy

This guide shows side-by-side comparisons for the most common factory_boy patterns and their seedling equivalents. factory_boy and seedling share several concepts (LazyAttribute, Sequence, SubFactory, RelatedFactory) but differ in two important ways:

  1. seedling is async-firstcreate() and create_batch() are coroutines.
  2. seedling passes a session — no global session state; the caller decides which session to use.

Defining a factory

factory_boy

python
import factory
from myapp.models import User

class UserFactory(factory.Factory):
    class Meta:
        model = User

    email = factory.Faker("email")
    name  = factory.Faker("name")

seedling

python
from seedling import Factory, Faker
from myapp.models import User

class UserFactory(Factory[User]):
    model = User
    email = Faker("email")
    name  = Faker("name")

The model class attribute replaces class Meta: model = ....


Building instances

factory_boy

python
user  = UserFactory.build()
users = UserFactory.build_batch(5)
data  = UserFactory.stub()            # dict-like object

seedling

python
user  = UserFactory.build()
users = UserFactory.build_batch(5)
data  = UserFactory.build_dict()      # plain dict

build() and build_batch() are synchronous in both libraries.


Persisting instances

factory_boy (with factory_boy-sqlalchemy)

python
from factory.alchemy import SQLAlchemyModelFactory

class UserFactory(SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session = db_session

    email = factory.Faker("email")

user  = UserFactory.create()
users = UserFactory.create_batch(5)

seedling

python
from seedling import Factory, Faker

class UserFactory(Factory[User]):
    model = User
    email = Faker("email")

user  = await UserFactory.create(session)
users = await UserFactory.create_batch(session, 5)

seedling always takes an explicit session argument — there is no global session to configure.


LazyAttribute

factory_boy

python
class UserFactory(factory.Factory):
    class Meta:
        model = User

    email    = factory.Faker("email")
    username = factory.LazyAttribute(lambda o: o.email.split("@")[0])

seedling

python
class UserFactory(Factory[User]):
    model    = User
    email    = Faker("email")
    username = LazyAttribute(lambda f: f["email"].split("@")[0])

seedling's LazyAttribute receives the partially-built field dict (f), not an object with attribute access. Use f["field_name"] to reference sibling fields.


Sequence

python
# factory_boy
code = factory.Sequence(lambda n: f"item-{n}")

# seedling (identical API)
from seedling import Sequence
code = Sequence(lambda n: f"item-{n}")

SubFactory

python
# factory_boy
class PostFactory(factory.Factory):
    class Meta:
        model = Post
    author = factory.SubFactory(UserFactory)

# seedling (identical API)
from seedling import SubFactory
class PostFactory(Factory[Post]):
    model  = Post
    author = SubFactory(UserFactory)

In seedling, SubFactory calls UserFactory.create(session) when the outer factory runs create(), and is skipped in build().


Traits

factory_boy

python
class UserFactory(factory.Factory):
    class Meta:
        model = User

    class Params:
        admin = factory.Trait(
            is_staff=True,
        )

user = UserFactory(admin=True)

seedling

python
from seedling import Factory, Trait

class UserFactory(Factory[User]):
    model    = User
    is_staff = False

    class admin(Trait):
        is_staff = True

user = UserFactory.build(admin=True)

seedling Trait classes are stackable: pass multiple booleans to combine them. trait_name=False suppresses a trait without forwarding the kwarg to the model.


@post_generation

factory_boy

python
class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    @factory.post_generation
    def groups(self, create, extracted, **kwargs):
        if not create or not extracted:
            return
        for group in extracted:
            self.groups.add(group)

seedling

python
from seedling import Factory, post_generation

class UserFactory(Factory[User]):
    model = User

    @post_generation
    async def assign_default_role(instance, session):
        await RoleFactory.create(session, user_id=instance.id)

Key differences:

  • seedling hooks are async by default (sync functions are also accepted).
  • The hook receives (instance, session), not (self, create, extracted).
  • There is no extracted parameter — use SubFactory or RelatedFactory instead.
  • Hooks fire after the instance is flushed and refreshed in the DB.

RelatedFactory

factory_boy

python
class AuthorFactory(factory.Factory):
    class Meta:
        model = Author

    default_book = factory.RelatedFactory(
        BookFactory,
        factory_related_name="author",
    )

seedling

python
from seedling import RelatedFactory

class AuthorFactory(Factory[Author]):
    model = Author
    default_book = RelatedFactory(
        BookFactory,
        author_id=lambda inst: inst.id,
    )

In seedling, callable kwargs receive the parent instance; non-callable kwargs are forwarded as-is.


Bulk insert

factory_boy has no built-in bulk insert path.

seedling

python
# Per-row (hooks fire, SubFactory resolved)
users = await UserFactory.create_batch(session, 1_000)

# Bulk — uses INSERT ... RETURNING, much faster for large batches
users = await UserFactory.create_batch(session, 100_000, bulk=True)

bulk=True limitations: @post_generation hooks and RelatedFactory / RelatedFactoryList do not fire. SubFactory and FK auto-resolve fields are omitted — supply them via overrides.


AutoFactory — no factory_boy equivalent

seedling's AutoFactory[T] introspects the SQLAlchemy mapper and generates sensible defaults automatically:

python
from seedling import AutoFactory

class UserFactory(AutoFactory[User]):
    model = User
    # email → faker.email(), name → faker.name(), etc. (smart defaults on by default)

There is no equivalent in factory_boy. It removes the need to declare every field explicitly for simple models.


Faker descriptor

factory_boy

python
email = factory.Faker("email")
code  = factory.Faker("numerify", text="###-##")
name  = factory.Faker("name", locale="fr_FR")

seedling

python
from seedling import Faker
email = Faker("email")
code  = Faker("numerify", text="###-##")
name  = Faker("name", locale="fr_FR")

Identical semantics; different import path.


Deterministic output

factory_boy

python
factory.random.reseed_random(42)

seedling

python
UserFactory.seed(42)   # seeds faker and all Iterator descriptors

What seedling adds beyond factory_boy

Featurefactory_boyseedling
Async create/create_batch
Explicit session argument✗ (global)
AutoFactory (mapper introspection)
Bulk insert path✓ (bulk=True)
State tracking + audit log
Seeder dependency runner
CLI (seed run, seed fresh, etc.)
SelfAttribute, Iterator, Skip