factory_boy Deep Dive: Python Test Data Done Right

factory_boy Deep Dive: Python Test Data Done Right

Test data is the silent killer of test suites. Hardcoded values drift out of sync with reality, fixtures become unmaintainable walls of JSON, and tests that share global state create debugging nightmares. factory_boy solves this by giving you a declarative, composable way to generate test data that stays close to your models and adapts as your schema evolves.

This guide covers everything from basic factory definitions to advanced patterns like traits, SubFactory relationships, and ORM integration.

Why factory_boy?

Before factory_boy, Python test suites typically fell into one of two traps:

  1. Hardcoded dictionaries scattered across test files that became stale and inconsistent
  2. Fixture files (JSON/YAML) that were a pain to maintain and required database reset between tests

factory_boy gives you a third path: factories as Python classes that mirror your models, generate realistic data by default, and let you override exactly what you care about in each test — nothing more.

pip install factory-boy

Defining Your First Factory

The most basic factory mirrors a data class or model:

import factory
from myapp.models import User

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

    first_name = "Jane"
    last_name = "Doe"
    email = "jane.doe@example.com"
    is_active = True

Build an instance without touching the database:

user = UserFactory.build()
assert user.email == "jane.doe@example.com"

Override specific fields inline:

admin = UserFactory.build(is_active=True, role="admin")

Sequences: Unique Values at Scale

The moment you build multiple objects, static values cause collisions. Sequences generate unique values automatically:

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

    # sequence yields 0, 1, 2, ...
    username = factory.Sequence(lambda n: f"user_{n}")
    email = factory.Sequence(lambda n: f"user{n}@example.com")

Each call to UserFactory.build() increments the counter:

u1 = UserFactory.build()  # username="user_0", email="user0@example.com"
u2 = UserFactory.build()  # username="user_1", email="user1@example.com"

You can reset sequences in tests that need deterministic output:

UserFactory.reset_sequence(0)

Lazy Attributes: Values That Depend on Other Fields

Sometimes a field's value should be derived from other fields set at build time. LazyAttribute solves this:

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

    first_name = factory.Faker("first_name")
    last_name = factory.Faker("last_name")
    # email is derived from first and last name
    email = factory.LazyAttribute(
        lambda obj: f"{obj.first_name.lower()}.{obj.last_name.lower()}@example.com"
    )
    username = factory.LazyAttribute(
        lambda obj: f"{obj.first_name[0].lower()}{obj.last_name.lower()}"
    )

LazyAttribute receives the object being built, with all previously-declared fields already set. This gives you computed fields without tight coupling in your actual model.

Faker Integration

factory_boy ships with deep Faker integration. Instead of static strings, generate realistic-looking data:

class ProfileFactory(factory.Factory):
    class Meta:
        model = Profile

    bio = factory.Faker("paragraph", nb_sentences=3)
    website = factory.Faker("url")
    phone = factory.Faker("phone_number")
    avatar_url = factory.Faker("image_url")
    birth_date = factory.Faker("date_of_birth", minimum_age=18, maximum_age=80)
    city = factory.Faker("city")
    country_code = factory.Faker("country_code")

Use factory.Faker with any provider from the Faker library. The locale can be set globally or per-factory:

class FrenchUserFactory(factory.Factory):
    class Meta:
        model = User
        exclude = ["locale"]

    locale = "fr_FR"
    name = factory.Faker("name", locale=factory.SelfAttribute("locale"))

SubFactory: Modeling Relationships

When your model has a foreign key or nested object, SubFactory lets you compose factories:

class AddressFactory(factory.Factory):
    class Meta:
        model = Address

    street = factory.Faker("street_address")
    city = factory.Faker("city")
    zip_code = factory.Faker("postcode")
    country = "US"

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

    name = factory.Faker("name")
    email = factory.Sequence(lambda n: f"user{n}@example.com")
    address = factory.SubFactory(AddressFactory)

Building a User automatically builds an Address. Override the nested object's fields using double-underscore notation:

# Override nested field
user = UserFactory.build(address__city="Berlin", address__country="DE")

# Pass in a pre-built object
existing_address = AddressFactory.build(city="Tokyo")
user = UserFactory.build(address=existing_address)

This double-underscore traversal works at any depth.

Traits: Named Presets for Common States

Traits let you define named configurations that can be toggled on:

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

    name = factory.Faker("name")
    email = factory.Sequence(lambda n: f"user{n}@example.com")
    is_active = True
    is_staff = False
    is_superuser = False
    email_verified = False

    class Params:
        admin = factory.Trait(
            is_staff=True,
            is_superuser=True,
        )
        verified = factory.Trait(
            email_verified=True,
        )
        inactive = factory.Trait(
            is_active=False,
        )

Apply traits at build time:

admin = UserFactory.build(admin=True)
verified_admin = UserFactory.build(admin=True, verified=True)
deactivated = UserFactory.build(inactive=True)

Traits can also include SubFactory overrides and LazyAttribute fields — they compose cleanly.

Django Integration: DjangoModelFactory

For Django projects, swap factory.Factory for factory.django.DjangoModelFactory. This enables .create() which persists to the database:

import factory
from factory.django import DjangoModelFactory
from myapp.models import Article, Author

class AuthorFactory(DjangoModelFactory):
    class Meta:
        model = Author

    name = factory.Faker("name")
    email = factory.Sequence(lambda n: f"author{n}@press.com")
    bio = factory.Faker("paragraph")

class ArticleFactory(DjangoModelFactory):
    class Meta:
        model = Article

    title = factory.Faker("sentence", nb_words=6)
    body = factory.Faker("paragraphs", nb=4, as_text=True)
    author = factory.SubFactory(AuthorFactory)
    published = True
    published_at = factory.Faker("date_time_this_year", tzinfo=timezone.utc)

Build strategies:

# No DB — fast, for unit tests
article = ArticleFactory.build()

# Saves to DB — for integration tests
article = ArticleFactory.create()

# Builds without saving, useful for form/serializer tests
article = ArticleFactory.build()

# Create a batch
articles = ArticleFactory.create_batch(10)

# Build a batch
articles = ArticleFactory.build_batch(5)

Handling Many-to-Many Relationships

Django M2M fields require a post-generation hook:

class ArticleFactory(DjangoModelFactory):
    class Meta:
        model = Article

    title = factory.Faker("sentence")

    @factory.post_generation
    def tags(self, create, extracted, **kwargs):
        if not create:
            return  # build() strategy — skip DB operations
        if extracted:
            for tag in extracted:
                self.tags.add(tag)

Usage:

tag1 = TagFactory.create(name="python")
tag2 = TagFactory.create(name="testing")
article = ArticleFactory.create(tags=[tag1, tag2])

Using get_or_create for Singletons

When a factory should avoid creating duplicates (e.g., roles, permission objects):

class RoleFactory(DjangoModelFactory):
    class Meta:
        model = Role
        django_get_or_create = ("name",)

    name = "viewer"

Calling RoleFactory.create(name="admin") twice returns the same object on the second call.

SQLAlchemy Integration

For SQLAlchemy, use factory.alchemy.SQLAlchemyModelFactory:

import factory
from factory.alchemy import SQLAlchemyModelFactory
from myapp.models import User, session

class UserFactory(SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session = session
        sqlalchemy_session_persistence = "commit"

    id = factory.Sequence(lambda n: n)
    name = factory.Faker("name")
    email = factory.Sequence(lambda n: f"user{n}@example.com")

sqlalchemy_session_persistence can be "flush" (visible in current transaction) or "commit" (persisted).

Transient Parameters

Sometimes you need parameters that influence how a factory builds an object but shouldn't map to model fields. Use Params with exclude:

class UserFactory(factory.Factory):
    class Meta:
        model = User
        exclude = ["superuser"]

    class Params:
        superuser = False

    name = factory.Faker("name")
    email = factory.Sequence(lambda n: f"user{n}@example.com")
    role = factory.LazyAttribute(
        lambda obj: "superuser" if obj.superuser else "viewer"
    )
    permissions = factory.LazyAttribute(
        lambda obj: ["all"] if obj.superuser else []
    )
su = UserFactory.build(superuser=True)
assert su.role == "superuser"

Patterns and Anti-Patterns

Do: Override Only What the Test Cares About

# Good — test focuses on the expired state
def test_expired_token_rejected():
    token = TokenFactory.create(expires_at=timezone.now() - timedelta(hours=1))
    assert not token.is_valid()

Don't: Duplicate Factories Per Test

Creating near-identical factories in each test file leads to drift. Define one canonical factory per model, use traits for variants.

Do: Keep Factories in a Dedicated Module

tests/
  factories/
    __init__.py
    user.py
    article.py
    order.py

Import from one place. Avoid defining factories inline in test files.

Don't: Use .create() When .build() Is Enough

create() hits the database. Unit tests don't need the database. Using .build() everywhere it's sufficient cuts test time dramatically.

Do: Use factory.Faker Over Hardcoded Strings

Static strings hide bugs. factory.Faker generates varied data that surfaces edge cases over time (names with apostrophes, long strings, unicode).

Testing the Factory Itself

Factories can drift from models as schemas change. A quick smoke test catches this:

def test_user_factory_creates_valid_user():
    user = UserFactory.create()
    assert user.pk is not None
    assert "@" in user.email

def test_article_factory_with_all_traits():
    article = ArticleFactory.create(featured=True, published=True)
    assert article.is_featured
    assert article.is_published

Run these as part of your CI suite to catch factory/model drift early.

Wrapping Up

factory_boy eliminates the two biggest pains of test data: duplication and rigidity. By defining factories close to your models, using LazyAttribute for computed fields, SubFactory for relationships, and traits for common states, you get a test data layer that scales with your application.

The discipline pays off when requirements change: update the factory once, and every test that uses it automatically gets the updated shape. Compare that to hunting down hardcoded dicts across 200 test files.

Start with simple factories, add sequences as soon as you need uniqueness, and reach for traits the moment you find yourself repeating override patterns across tests.

Start now free