Skip to main content

🏗️ Pydantic Builder

📋 Overview

Pydantic Builder is a micro-framework that simplifies the creation of test fixtures and object construction for Pydantic models. It implements a type-safe builder pattern that allows you to construct complex Pydantic model instances with a clean, fluent API.

✨ Features

  • 🔒 Type-safe builder pattern with full generic type support
  • 🔗 Fluent API with method chaining for readable object construction
  • 🎯 Focus on what matters - only specify the fields you care about, use defaults for the rest
  • 🧪 Perfect for testing - reduces boilerplate in test fixtures
  • 💡 IDE-friendly - full autocompletion support for all model fields
  • 🎨 Clean test code - keeps tests focused on what's being tested, not setup

📥 Installation

💡 The package is available on PyPI:

pip install pydantic_builder

Requirements: Python >= 3.10

🚀 Usage

🔰 Basic Usage

Create a builder for any Pydantic model by extending AbstractBaseBuilder:

from pydantic import BaseModel
from pydantic_builder import AbstractBaseBuilder

# Your Pydantic model
class Point(BaseModel):
    x: int
    y: int

# Create a builder for it
class PointBuilder(AbstractBaseBuilder[Point]):
    @property
    def default_instance(self) -> Point:
        return Point(x=0, y=0)

# Use the builder
point = PointBuilder().build()
# Point(x=0, y=0)

# Override specific fields
point = PointBuilder().with_(x=10).build()
# Point(x=10, y=0)

# Chain multiple field updates
point = PointBuilder().with_(x=5).with_(y=3).build()
# Point(x=5, y=3)

🌟 Intermediate Usage - Simplifying Tests

The builder pattern really shines in tests, where you want to focus on specific scenarios without cluttering your test with irrelevant setup:

from pydantic import BaseModel
from pydantic_builder import AbstractBaseBuilder

class Range(BaseModel):
    min: float | None = None
    max: float | None = None
    
    def size(self) -> float:
        if self.min is None or self.max is None:
            return float("inf")
        return self.max - self.min

class RangeBuilder(AbstractBaseBuilder[Range]):
    @property
    def default_instance(self) -> Range:
        return Range(min=0, max=5)

# In your tests - only specify what's relevant to the test
def test_size_with_no_upper_bound():
    # Clear and focused - only the max=None matters for this test
    range_ = RangeBuilder().with_(max=None).build()
    assert range_.size() == float("inf")

def test_size_with_specific_bounds():
    # Chain multiple attributes cleanly
    range_ = RangeBuilder().with_(min=1).with_(max=10).build()
    assert range_.size() == 9

🔥 Advanced Usage - Composing Builders

For complex models with nested structures, builders can be composed together:

from pydantic import BaseModel
from pydantic_builder import AbstractBaseBuilder

class Ingredient(BaseModel):
    id: int
    name: str
    quantity: float
    unit: str
    optional: bool = False

class Recipe(BaseModel):
    title: str
    serving_size: int
    preparation_time: float
    cooking_time: float
    ingredients: list[Ingredient]
    steps: list[str] = []

    def scale_recipe(self, new_serving_size: int) -> Self:
        """Scale recipe ingredients based on new serving size."""
        if self.serving_size is None or self.serving_size == 0:
            raise ValueError("Cannot scale recipe without original serving size")

        scale_factor = new_serving_size / self.serving_size

        scaled_ingredients = []
        for ingredient in self.ingredients:
            scaled_ingredient = Ingredient(
                id=ingredient.id,
                name=ingredient.name,
                quantity=ingredient.quantity * scale_factor,
                unit=ingredient.unit,
                optional=ingredient.optional,
            )
            scaled_ingredients.append(scaled_ingredient)

        return Recipe(
            **self.model_dump(exclude={"serving_size", "ingredients"}),
            ingredients=scaled_ingredients,
            serving_size=new_serving_size,
        )

class IngredientBuilder(AbstractBaseBuilder[Ingredient]):
    @property
    def default_instance(self) -> Ingredient:
        return Ingredient(id=1, name="flour", quantity=100, unit="g")

class RecipeBuilder(AbstractBaseBuilder[Recipe]):
    @property
    def default_instance(self) -> Recipe:
        return Recipe(
            title="Default Recipe",
            serving_size=4,
            preparation_time=30,
            cooking_time=60,
            ingredients=[
                Ingredient(id=1, name="flour", quantity=100, unit="g"),
                Ingredient(id=2, name="sugar", quantity=50, unit="g"),
            ],
            steps=["Mix ingredients", "Bake for 30 minutes"],
        )

# Compose builders together for complex test scenarios
def test_recipe_with_custom_ingredients():
    recipe = (
        RecipeBuilder()
        .with_(
            title="Custom Cake",
            ingredients=[
                IngredientBuilder().with_(name="flour", quantity=200).build(),
                IngredientBuilder().with_(name="sugar", quantity=150).build(),
                IngredientBuilder().with_(name="eggs", quantity=3, unit="units").build(),
            ]
        )
        .build()
    )
    
    assert recipe.title == "Custom Cake"
    assert len(recipe.ingredients) == 3
    assert recipe.ingredients[0].quantity == 200


def test_recipe_scaling():

    # Arrange
    original_serving_size = 2 # 1st important value
    original_ingredient_quantity = 100 # 2nd important value
    new_serving_size = original_serving_size * 2 # 3rd important value

    recipe = RecipeBuilder()
    .with_(
        serving_size=original_serving_size # 1st important value
    )
    .with_(
        ingredients=[
            IngredientBuilder()
            .with_(
                quantity=original_ingredient_quantity # 2nd important value
            )
            .build()
            ]
    )
    .build()

    # Act
    scaled_recipe = recipe.scale_recipe(new_serving_size) # 3rd important value

    # Assert
    assert scaled_recipe.serving_size == new_serving_size
    assert scaled_recipe.ingredients[0].quantity == original_ingredient_quantity * 2
    

🎯 Why Use Builders?

Without builders:

def test_ingredient_conversion():
    # Have to specify every field, even irrelevant ones
    ingredient = Ingredient(
        id=1, # Don't care about id for this test
        name="flour",  # Don't care about name for this test
        quantity=1,
        unit="kg",
        optional=False  # Don't care about optional for this test
    )
    assert ingredient.convert_to_grams().quantity == 1000

With builders:

def test_ingredient_conversion():
    # Only specify what matters for this test
    ingredient = IngredientBuilder().with_(quantity=1, unit="kg").build()
    assert ingredient.convert_to_grams().quantity == 1000

🔍 Going Further

  • 🧪 See the test files for comprehensive usage examples
  • 💬 Open an issue for questions or feature requests

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

Metadata

Release files for pydantic_builder 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pydantic_builder 0.1.0
File Size Uploaded
pydantic_builder-0.1.0.tar.gz 6.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pydantic_builder 0.1.0
File Interpreter ABI Platform
pydantic_builder-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 13.3 kB

Release files / pydantic_builder-0.1.0.tar.gz

Download URL pydantic_builder-0.1.0.tar.gz
Size 6.1 kB
Tags Source
SHA-256 checksum
How to use checksums
7fec057cc40cd256c02ea2452e2016e0dc14ef37a5c0f38cf1b2f65fe502d5e2
BLAKE2b-256 checksum
How to use checksums
a56bbe6c35dbb5a22e988b8c2e4621b1fb9e4a4a1c319701d3fd038b295f6338
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.15

Release files / pydantic_builder-0.1.0-py3-none-any.whl

Download URL pydantic_builder-0.1.0-py3-none-any.whl
Size 7.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
327b3c6162af33bb5ea0da37557a837f525bac281ceaf442295c1550bfe99c90
BLAKE2b-256 checksum
How to use checksums
9a62d8874003f44641e6c63ff2ad9c778b552eb79bd4b42adfdb41d08129d5f2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.15

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page