Skip to content

Factories

A factory describes what one row of a table looks like when nobody cares about the exact values — a name, an email, a title, a date somewhere in the last year. Seeders and tests then ask for as many of those rows as they need, in whatever shape the case calls for.

database/factories/user_factory.py
from typing import Any
from almasix.hashing import Hash
from almasix.orm import Factory
from app.models.user import User
class UserFactory(Factory):
model = User
def definition(self) -> dict[str, Any]:
return {
"name": self.fake.name(),
"email": self.fake.unique().safe_email(),
"password": Hash.make("password"),
}
user = await User.factory().create()
users = await User.factory().count(10).create()

Because Almasix’s ORM is async, make() and create() are coroutines. The rest of the builder — count, state, has, for_ — is ordinary chaining and needs no await.

Terminal window
python smith make:factory PostFactory
python smith make:factory PostFactory --model Post
python smith make:model Post -m -f # model + migration + factory

Factories live in database/factories/, one class per file, named after the model: PostFactory in post_factory.py.

Add the HasFactory mixin — models generated by make:model already have it:

from almasix.orm import HasFactory, Model
class Post(HasFactory, Model):
fillable = ("title", "user_id")

Post.factory() then finds PostFactory by name. Three shorthands:

Post.factory() # a builder for one post
Post.factory(3) # three posts
Post.factory({"title": "Fixed"}) # one post, with a state
Post.factory(3, {"title": "Fixed"}) # three of them

To name the factory yourself, override new_factory():

class Post(HasFactory, Model):
@classmethod
def new_factory(cls):
return ArchivedPostFactory.new()

self.fake is a small generator with the providers definitions actually reach for. It is seedable, so a seeded run produces the same database twice:

self.fake.name() # "Ada Lovelace"
self.fake.unique().safe_email() # never repeats within a run
self.fake.sentence(8)
self.fake.paragraph()
self.fake.number_between(1, 100)
self.fake.boolean(30) # true 30% of the time
self.fake.random_element(["draft", "published"])
self.fake.date_time_between(start, end)
self.fake.city(), self.fake.country(), self.fake.phone_number()

Laravel’s camelCase spellings work too (self.fake.safeEmail()), so a definition ported from PHP runs unchanged.

Want the full Faker catalogue? Install it and hand it to Almasix once, at boot:

import faker
from almasix.orm import Fake
Fake.resolve_using(lambda: faker.Faker("en_GB"))

Every factory built afterwards reaches that generator through self.fake.

A state is a dict, or a callable given the attributes so far. Name them as methods and they read like the model:

class PostFactory(Factory):
model = Post
def definition(self) -> dict[str, Any]:
return {"title": self.fake.title(), "published": True, "views": 0}
def draft(self) -> Factory:
return self.state({"published": False})
def popular(self) -> Factory:
return self.state(lambda attributes: {"views": attributes["views"] + 5_000})
await Post.factory().draft().popular().create()
await Post.factory().state({"title": "Fixed"}).create()
await Post.factory().set("title", "Fixed").create()

A state callable may take the attributes, or the attributes and the parent model being built for, and it may be async.

Attributes passed straight to make() or create() are a state as well, and they are applied last:

await Post.factory().draft().create({"title": "Overrides everything"})
# Alternates as it goes.
await Post.factory().count(6).sequence(
{"published": True},
{"published": False},
).create()
# One row per step — the sequence sets the count.
await Post.factory().for_each_sequence(
{"title": "one"},
{"title": "two"},
).create()
# Every combination: four posts here.
await Post.factory().count(4).cross_join_sequence(
[{"title": "a"}, {"title": "b"}],
[{"published": True}, {"published": False}],
).create()

A step may be a callable, which is given the sequence itself — index counts the calls so far:

await Post.factory().count(3).sequence(
lambda sequence: {"title": f"Post {sequence.index + 1}"},
).create()

has() creates children after the parent exists; for_() supplies the parent before the child is written.

# A user with three posts.
await User.factory().has(Post.factory().count(3)).create()
# The relation name is guessed from the model; name it when the guess is wrong.
await User.factory().has(Post.factory().count(3), "posts").create()
# Three posts, one author between them.
await Post.factory().count(3).for_(User.factory(), "author").create()
# An author that already exists.
await Post.factory().count(3).for_(ada, "author").create()

Many-to-many relations attach with pivot columns:

await User.factory().has_attached(Role.factory().count(2), {"level": "lead"}, "roles").create()
await User.factory().has_attached(existing_roles, lambda role: {"level": role.name}, "roles").create()

Nesting works the way you would write it by hand:

await User.factory().has(
Post.factory().count(2).has(Comment.factory().count(3), "comments"),
"posts",
).create()
await User.factory().has_posts(3).create()
await User.factory().has_posts(3, {"published": False}).create()
await Post.factory().for_author({"name": "Ada"}).create()

has_<relation> and for_<relation> build the related model’s factory for you. They are the same two methods above, spelled shorter.

Left alone, every for_() in a batch creates its own parent. recycle() hands the whole graph a pool of existing models to draw from instead:

ada = await User.factory().create()
await Post.factory().count(10).recycle(ada).for_(User.factory(), "author").create()
# ten posts, all of them Ada's, and no eleventh user

A factory used as an attribute value resolves to that model’s key, and honors the same pool:

def definition(self) -> dict[str, Any]:
return {"title": self.fake.title(), "user_id": UserFactory.new()}
class UserFactory(Factory):
model = User
def configure(self) -> Factory:
return self.after_creating(self.send_welcome)
async def send_welcome(self, user: User) -> None:
await user.notify(WelcomeNotification())

after_making(callback) runs on every model the factory builds; after_creating(callback) runs after each one is saved and may take the parent as a second argument. Callbacks may be sync or async.

await Post.factory().raw() # the attributes, no model
await Post.factory().count(2).raw() # a list of them
await Post.factory().make_one() # one, unsaved
await Post.factory().make_many(3)
await Post.factory().create_one()
await Post.factory().create_many([{"title": "a"}, {"title": "b"}])
await Post.factory().create_quietly() # no model events
await Post.factory().trashed().create() # arrives soft deleted
await Post.factory().connection("reporting").create()
later = Post.factory().lazy({"title": "Written when called"})
post = await later()

Seeders are the primary consumer. The fixed columns a demo asserts on are states; the factory invents the rest:

database/seeders/demo_seeder.py
from almasix.orm import Seeder
from app.models.post import Post
from app.models.user import User
class DemoSeeder(Seeder):
async def run(self) -> None:
ada = await User.factory().create({"email": "ada@almasix.dev", "name": "Ada"})
await Post.factory().count(3).for_(ada, "author").create()
await User.factory().count(10).has(Post.factory().count(2), "posts").create()
Terminal window
python smith db:seed
python smith migrate:fresh --seed

See Seeding for the seeder API itself.

  • make() and create() are coroutines. Everything that touches the database in Almasix is async; the builder methods are not.
  • for_ and has_attached carry a trailing underscore or an explicit relation name where PHP writes for and hasAttached. for is a Python keyword; for_ is the closest legal spelling.
  • Factories write past the mass assignment guard. Almasix models are guarded by default, so a factory fills attributes directly rather than relying on fillable. That is what Laravel’s factories effectively do too.
  • Fake data ships with the framework instead of requiring Faker. The provider list is smaller; Fake.resolve_using swaps in the real thing.
  • A created parent’s relations are not loaded. has() writes the children and stops there — read them back with await user.load("posts") when you need them in memory.