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:
- Hardcoded dictionaries scattered across test files that became stale and inconsistent
- 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-boyDefining 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 = TrueBuild 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.pyImport 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_publishedRun 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.