Skip to content

Usage

Quick Start

Suppose we have two dataclasses like this:

from dataclasses import dataclass

@dataclass
class Spam:
    a: int
    b: int
    c: int

@dataclass
class Bacon:
    x: int
    y: list[str]
    z: Spam
    t: str = "Francis"

We can create factories for them using these factories:

from factorio import fields
from factorio.factories import Factory

class SpamFactory(Factory[Spam]):
    a = fields.IntegerField(max_value=42)
    b = fields.ChoiceField(range(21))
    c = fields.ConstantField(1024)

class BaconFactory(Factory[Bacon]):
    x = fields.IntegerField(max_value=4)
    y = fields.ListField(fields.StringField(max_chars=4), length=5, variation=2)
    z = fields.FactoryField(SpamFactory)

Then using the factories is as simple as:

bacon = BaconFactory.build()

assert 0 <= bacon.x <= 4
assert isinstance(bacon.y, list)
assert 0 <= bacon.z.a <= 42
assert bacon.t == "Francis"

It is possible to override specific fields:

bacon = BaconFactory.build(x=400, t="Kevin")

assert bacon.x == 400
assert bacon.z.c == 1024
assert bacon.t == "Kevin"

Contrary to other common factory libraries, .build() is needed to create an object. This is by design, as we feel that it is more explicit and less error prone. MyModelFactory() should only be reserved to create an actual MyModelFactory object and never for a MyModel object.

Also, another difference with other factory libraries, is that this will never try to save the object to the database. The reason is that we feel that this is not the responsibility of the factory, and actually should be done by the test itself.


TextField Deep Dive

TextField is one of the most powerful features in factorio. It dynamically delegates to Faker providers to generate realistic, domain-specific data.

How It Works

TextField takes a text type name, converts it to lowercase, replaces spaces/hyphens with underscores, and calls the corresponding Faker method:

from factorio import fields

class UserFactory(Factory[User]):
    # These all work:
    email = fields.TextField("email")           # faker.email()
    company = fields.TextField("Company")       # faker.company() (case-insensitive)
    ip_address = fields.TextField("ipv4")       # faker.ipv4()
    full_name = fields.TextField("First Name")  # faker.first_name() (spaces → underscores)

Passing Arguments to Faker

You can pass additional keyword arguments that get forwarded to the Faker method:

class UserFactory(Factory[User]):
    # Custom email domain
    work_email = fields.TextField("safe_email", domain="company.com")

    # Paragraph with specific number of sentences
    bio = fields.TextField("paragraph", nb_sentences=3)

    # Integer with specific range (via Faker's pyint)
    lucky_number = fields.TextField("pyint", min_value=1, max_value=100)

Common Text Types

Personal Information: - "name", "first_name", "last_name" - "email", "safe_email", "free_email" - "phone_number", "msisdn"

Addresses: - "address", "street_address", "city", "country" - "postcode", "state", "state_abbr"

Internet: - "ipv4", "ipv6", "url", "domain_name" - "user_name", "password", "mac_address"

Business: - "company", "job", "bs", "catch_phrase" - "ein", "duns"

Finance: - "credit_card_number", "iban", "swift", "bic" - "currency_code", "currency_name", "price"

Text Generation: - "paragraph", "sentence", "word", "text" - "slug", "locale"

See Faker's Providers Documentation for the complete list of available providers.


Advanced Field Patterns

Collection Field Variation

Collection fields (ListField, TupleField, SetField, DictField) support a variation parameter to randomize their length:

class PostFactory(Factory[Post]):
    # Fixed length: always 5 tags
    fixed_tags = fields.ListField(
        fields.TextField("word"),
        length=5
    )

    # Variable length: 3-7 tags (5 ± 2)
    variable_tags = fields.ListField(
        fields.TextField("word"),
        length=5,
        variation=2
    )

    # Wide variation: 1-9 items (5 ± 4)
    flexible_items = fields.SetField(
        fields.IntegerField(min_value=1, max_value=100),
        length=5,
        variation=4
    )

post = PostFactory.build()
assert len(post.fixed_tags) == 5
assert 3 <= len(post.variable_tags) <= 7

BooleanField Truth Probability

Control the likelihood of True/False values:

class UserFactory(Factory[User]):
    # 50% chance (default)
    is_active = fields.BooleanField()

    # Only 5% chance of being admin
    is_admin = fields.BooleanField(truth_probability=5)

    # 95% chance of being verified
    is_verified = fields.BooleanField(truth_probability=95)

    # Always False
    is_deleted = fields.BooleanField(truth_probability=0)

    # Always True
    is_registered = fields.BooleanField(truth_probability=100)

DecimalField Precision Control

Fine-tune decimal places with accuracy and variation:

from decimal import Decimal

class ProductFactory(Factory[Product]):
    # Exactly 2 decimal places (for currency)
    price = fields.DecimalField(
        min_value=Decimal("0.01"),
        max_value=Decimal("999.99"),
        accuracy=2,
        variation=0
    )

    # 4-6 decimal places (for scientific measurements)
    measurement = fields.DecimalField(
        min_value=0,
        max_value=100,
        accuracy=5,
        variation=1
    )

product = ProductFactory.build()
# price might be: Decimal('45.67')
# measurement might be: Decimal('23.45678')

CharField Alphabet Customization

Generate characters from custom alphabets:

class CodeFactory(Factory[Code]):
    # Lowercase only (a-z)
    lowercase = fields.CharField()

    # Mixed case (a-z, A-Z)
    mixed = fields.CharField(include_uppercase=True)

    # Alphanumeric (a-z, A-Z, 0-9)
    alphanumeric = fields.CharField(
        include_uppercase=True,
        include_digits=True
    )

    # Digits only (0-9) - use CharField + digits
    digit_only = fields.CharField(include_digits=True)

code = CodeFactory.build()
assert code.lowercase in "abcdefghijklmnopqrstuvwxyz"
assert code.alphanumeric in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

TimezoneField Geographic Filtering

Filter timezones by geographic region:

from zoneinfo import ZoneInfo

class UserFactory(Factory[User]):
    # Any timezone worldwide
    any_timezone = fields.TimezoneField()

    # Only European timezones
    eu_timezone = fields.TimezoneField(areas=("Europe",))

    # American or European timezones
    western_timezone = fields.TimezoneField(
        areas=("America", "Europe")
    )

    # Asia-Pacific region
    apac_timezone = fields.TimezoneField(
        areas=("Asia", "Australia", "Pacific")
    )

    # UTC only (via "Etc" area)
    utc_timezone = fields.TimezoneField(areas=("Etc",))

user = UserFactory.build()
assert user.eu_timezone.key.startswith("Europe/")
assert user.apac_timezone.key.split("/")[0] in ("Asia", "Australia", "Pacific")

Available geographic areas: - "Africa", "America", "Antarctica", "Arctic", "Asia" - "Atlantic", "Australia", "Europe", "Indian", "Pacific" - "Etc" (includes UTC and special zones)


Field Override Techniques

Override with Values

The simplest way to override fields is by passing values directly:

class UserFactory(Factory[User]):
    name = fields.TextField("name")
    age = fields.IntegerField(min_value=18, max_value=65)
    email = fields.TextField("email")

# Override specific fields
user = UserFactory.build(
    name="John Doe",
    age=30,
    email="john@example.com"
)

assert user.name == "John Doe"
assert user.age == 30

Override with Field Instances

For dynamic ranges or conditional logic, override with field instances:

# Generate users in specific age ranges
young_user = UserFactory.build(
    age=fields.IntegerField(min_value=18, max_value=25)
)

senior_user = UserFactory.build(
    age=fields.IntegerField(min_value=60, max_value=65)
)

# Dynamic based on test context
def create_user_for_region(region: str):
    if region == "EU":
        return UserFactory.build(
            age=fields.IntegerField(min_value=18, max_value=100)  # GDPR allows all ages
        )
    else:
        return UserFactory.build(
            age=fields.IntegerField(min_value=18, max_value=65)
        )

Combining Overrides

Mix value overrides and field overrides:

user = UserFactory.build(
    name="Jane Smith",  # Static value
    age=fields.IntegerField(min_value=25, max_value=30),  # Dynamic range
    email="jane@test.com"  # Static value
)

Nested Factories

Basic FactoryField

Use FactoryField to generate nested objects:

from dataclasses import dataclass
from factorio import fields
from factorio.factories import Factory

@dataclass
class Address:
    street: str
    city: str
    zipcode: str

@dataclass
class User:
    name: str
    email: str
    address: Address

class AddressFactory(Factory[Address]):
    street = fields.TextField("street_address")
    city = fields.TextField("city")
    zipcode = fields.TextField("postcode")

class UserFactory(Factory[User]):
    name = fields.TextField("name")
    email = fields.TextField("email")
    address = fields.FactoryField(AddressFactory)

user = UserFactory.build()
assert isinstance(user.address, Address)
assert isinstance(user.address.city, str)

Complex Object Graphs

Nest factories multiple levels deep:

@dataclass
class Country:
    name: str
    code: str

@dataclass
class Address:
    street: str
    city: str
    country: Country

@dataclass
class User:
    name: str
    address: Address

class CountryFactory(Factory[Country]):
    name = fields.TextField("country")
    code = fields.TextField("country_code")

class AddressFactory(Factory[Address]):
    street = fields.TextField("street_address")
    city = fields.TextField("city")
    country = fields.FactoryField(CountryFactory)

class UserFactory(Factory[User]):
    name = fields.TextField("name")
    address = fields.FactoryField(AddressFactory)

user = UserFactory.build()
assert isinstance(user.address.country, Country)
assert isinstance(user.address.country.code, str)

Overriding Nested Fields

Use double underscore notation to override nested factory fields:

# Override nested address fields
user = UserFactory.build(
    address__city="Custom City",
    address__country__name="Custom Country"
)

assert user.address.city == "Custom City"
assert user.address.country.name == "Custom Country"

Note: This requires your model to support **kwargs initialization or you need to handle nested overrides manually.


Factory Inheritance

Extending Base Factories

Create base factories and extend them:

class BaseUserFactory(Factory[User]):
    name = fields.TextField("name")
    email = fields.TextField("email")
    is_active = fields.BooleanField(truth_probability=90)

class AdminUserFactory(BaseUserFactory):
    is_admin = fields.ConstantField(True)
    role = fields.ConstantField("administrator")

class RegularUserFactory(BaseUserFactory):
    is_admin = fields.ConstantField(False)
    role = fields.ChoiceField(["user", "member", "subscriber"])

admin = AdminUserFactory.build()
assert admin.is_admin is True
assert admin.role == "administrator"

regular = RegularUserFactory.build()
assert regular.is_admin is False
assert regular.role in ["user", "member", "subscriber"]

Overriding Parent Fields

Child factories can override parent field definitions:

class BaseProductFactory(Factory[Product]):
    name = fields.TextField("word")
    price = fields.DecimalField(min_value=1, max_value=100, accuracy=2)
    category = fields.ChoiceField(["general", "misc"])

class PremiumProductFactory(BaseProductFactory):
    # Override price range for premium products
    price = fields.DecimalField(min_value=100, max_value=1000, accuracy=2)

    # Override category
    category = fields.ConstantField("premium")

    # Add new field
    warranty_years = fields.IntegerField(min_value=2, max_value=5)

premium = PremiumProductFactory.build()
assert 100 <= premium.price <= 1000
assert premium.category == "premium"
assert 2 <= premium.warranty_years <= 5

Multiple Inheritance

Combine multiple base factories (use with caution):

class TimestampedFactory(Factory[T]):
    created_at = fields.DateTimeField()
    updated_at = fields.DateTimeField()

class AuditableFactory(Factory[T]):
    created_by = fields.TextField("user_name")
    updated_by = fields.TextField("user_name")

class ProductFactory(TimestampedFactory, AuditableFactory):
    name = fields.TextField("word")
    price = fields.DecimalField(min_value=1, max_value=100)

product = ProductFactory.build()
assert hasattr(product, 'created_at')
assert hasattr(product, 'created_by')
assert hasattr(product, 'name')

Warning: Multiple inheritance can lead to conflicts if both parents define the same fields. Python's MRO (Method Resolution Order) determines which field wins.


Design Philosophy

Explicit .build() Method

Unlike some factory libraries, factorio requires explicit .build() calls:

# ✅ Correct - explicit object creation
user = UserFactory.build()

# ❌ Wrong - this creates a Factory instance, not a User
user = UserFactory()  # Returns UserFactory object

Why? This makes code more explicit and prevents accidental factory instantiation when you meant to create model objects.

No Database Interaction

factorio never saves objects to databases:

# Creates object in memory only
user = UserFactory.build()

# You must explicitly save if needed
session.add(user)
session.commit()

Why? Separation of concerns - factories should generate data, tests should handle persistence. This makes tests faster and more predictable.

Realistic Data with Faker

Under the hood, factorio uses Faker to generate realistic test data:

# Instead of unrealistic data like:
user = User(name="test1", email="test@example.com")

# You get realistic data:
user = UserFactory.build()
# user.name might be "Sarah Johnson"
# user.email might be "sarah.johnson@company.net"

This helps catch bugs that only appear with realistic data patterns.