factory_boy Advanced: Traits, Sub-Factories, Batch Creation, and Django Integration

factory_boy Advanced: Traits, Sub-Factories, Batch Creation, and Django Integration

factory_boy is Python's most widely used test object factory library. The basics — factory.Factory and factory.django.DjangoModelFactory — are well documented, but the advanced features are what make large test suites maintainable. This guide covers traits, sub-factories, batch creation, lazy attributes, and the patterns that keep factories composable as your models grow.

Setup

pip install factory-boy

For Django:

# In conftest.py or a dedicated factories.py file
import factory
from factory.django import DjangoModelFactory
from myapp.models import User, Profile, Post

Traits

Traits are named flag configurations that toggle sets of fields. They replace the need for multiple separate factory subclasses.

class UserFactory(DjangoModelFactory):
    class Meta:
        model = User

    username = factory.Sequence(lambda n: f'user_{n}')
    email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
    is_active = True
    is_staff = False
    is_superuser = False

    class Params:
        # Defining traits
        admin = factory.Trait(
            is_staff=True,
            is_superuser=True,
        )
        inactive = factory.Trait(
            is_active=False,
        )
        staff = factory.Trait(
            is_staff=True,
        )

Usage:

user = UserFactory()                      # regular active user
admin = UserFactory(admin=True)           # staff + superuser
inactive_admin = UserFactory(admin=True, inactive=True)  # both traits

Traits can reference each other and set any number of fields. They compose cleanly:

class PostFactory(DjangoModelFactory):
    class Meta:
        model = Post

    title = factory.Sequence(lambda n: f'Post {n}')
    author = factory.SubFactory(UserFactory)
    status = 'draft'
    published_at = None

    class Params:
        published = factory.Trait(
            status='published',
            published_at=factory.LazyFunction(timezone.now),
        )
        featured = factory.Trait(
            is_featured=True,
        )

Lazy Attributes

factory.LazyAttribute computes a field value from other fields on the same object:

class ProfileFactory(DjangoModelFactory):
    class Meta:
        model = Profile

    user = factory.SubFactory(UserFactory)
    display_name = factory.LazyAttribute(lambda obj: obj.user.username.title())
    bio = factory.LazyAttribute(lambda obj: f'Hello, I am {obj.display_name}')
    avatar_url = factory.LazyAttribute(
        lambda obj: f'https://avatars.example.com/{obj.user.username}.jpg'
    )

factory.LazyFunction is for callables that don't need access to the object:

class EventFactory(DjangoModelFactory):
    class Meta:
        model = Event

    name = factory.Sequence(lambda n: f'Event {n}')
    start_date = factory.LazyFunction(timezone.now)
    end_date = factory.LazyAttribute(
        lambda obj: obj.start_date + timedelta(hours=2)
    )

Sub-Factories

factory.SubFactory creates related objects automatically:

class CommentFactory(DjangoModelFactory):
    class Meta:
        model = Comment

    post = factory.SubFactory(PostFactory)
    author = factory.SubFactory(UserFactory)
    body = factory.Sequence(lambda n: f'Comment {n} body text')
    created_at = factory.LazyFunction(timezone.now)

Building a comment automatically creates a post and a user. Override at any level:

# Use an existing post
post = PostFactory()
comment = CommentFactory(post=post)

# Override the sub-factory's fields
comment = CommentFactory(post__title='Custom Title', author__username='alice')

The double-underscore syntax (post__title) passes overrides into the sub-factory.

For reverse relations (one-to-many) and many-to-many, use factory.RelatedFactory and factory.post_generation:

class UserFactory(DjangoModelFactory):
    class Meta:
        model = User

    username = factory.Sequence(lambda n: f'user_{n}')

    # Creates a Profile after the User is saved
    profile = factory.RelatedFactory(
        'myapp.tests.factories.ProfileFactory',
        factory_related_name='user',
    )

For M2M:

class PostFactory(DjangoModelFactory):
    class Meta:
        model = Post

    title = factory.Sequence(lambda n: f'Post {n}')

    @factory.post_generation
    def tags(self, create, extracted, **kwargs):
        if not create:
            return  # In build mode, skip M2M
        if extracted:
            for tag in extracted:
                self.tags.add(tag)

Usage:

tag1 = TagFactory()
tag2 = TagFactory()
post = PostFactory(tags=[tag1, tag2])

Batch Creation

factory_boy has built-in batch helpers:

# Build (no DB) a list of 10 users
users = UserFactory.build_batch(10)

# Create (DB) a list of 5 posts with a shared author
author = UserFactory()
posts = PostFactory.create_batch(5, author=author)

# Create with a trait
admins = UserFactory.create_batch(3, admin=True)

build_batch and create_batch accept the same overrides as build() and create().

Sequences

factory.Sequence provides auto-incrementing values:

class ProductFactory(DjangoModelFactory):
    class Meta:
        model = Product

    sku = factory.Sequence(lambda n: f'SKU-{n:04d}')
    name = factory.Sequence(lambda n: f'Product {n}')
    price = factory.Sequence(lambda n: Decimal(f'{n * 10}.99'))

Reset sequences between tests if you need predictable values:

# In pytest conftest.py
@pytest.fixture(autouse=True)
def reset_sequences():
    factory.reset_sequence()

Django Model Factory Patterns

Avoiding Database Hits in Unit Tests

Use build() (not create()) when your unit tests don't need database-saved objects:

def test_display_name_formatting():
    user = UserFactory.build(username='alice_w')
    profile = ProfileFactory.build(user=user)
    assert profile.display_name == 'Alice_W'

build() creates the object in memory, populates all fields, and runs LazyAttribute and SubFactory in build mode (sub-factories also use build()).

Using create_strategy

If you need a factory that always uses build() mode:

class InMemoryUserFactory(UserFactory):
    class Meta:
        strategy = factory.BUILD_STRATEGY

exclude for Transient Fields

Use exclude for fields that affect factory logic but shouldn't be passed to the model:

class OrderFactory(DjangoModelFactory):
    class Meta:
        model = Order
        exclude = ['with_items']

    class Params:
        with_items = False

    total = factory.LazyAttribute(
        lambda obj: Decimal('99.99') if obj.with_items else Decimal('0.00')
    )

    @factory.post_generation
    def items(self, create, extracted, **kwargs):
        if not create:
            return
        if self.with_items or extracted:
            items = extracted or [OrderItemFactory(order=self)]
            for item in items:
                self.items.add(item)

pytest Integration

# conftest.py
import pytest
from myapp.tests.factories import UserFactory, PostFactory

@pytest.fixture
def user(db):
    return UserFactory()

@pytest.fixture
def admin_user(db):
    return UserFactory(admin=True)

@pytest.fixture
def published_posts(db, user):
    return PostFactory.create_batch(3, author=user, published=True)

Using db fixture (from pytest-django) ensures the factory runs inside a transaction that rolls back after each test.

Key Points

  • Traits (class Params with factory.Trait) replace factory subclasses for common field variations
  • factory.LazyAttribute computes fields from other fields on the same object; factory.LazyFunction for callables that don't need the object
  • factory.SubFactory creates related objects automatically; override nested fields with field__nested_field=value
  • factory.RelatedFactory and @factory.post_generation handle reverse relations and M2M
  • create_batch(n) creates multiple database records with shared overrides
  • Use build() in unit tests to avoid database overhead; use create() in integration tests
  • Reset sequences with factory.reset_sequence() for predictable values in ordered assertions

Read more