Sitelet https://thinkinginpython.com/27_Factory.html
Contents
Chapter 27

Factory

When you discover that you need to add new types to a system, the most sensible first step is to use polymorphism to create a common interface to those new types. This separates the rest of the code in your system from the knowledge of the specific types that you are adding. You may add new types without disturbing existing code … or so it seems. At first it appears that the only place you need to change the code in such a design is the place where you inherit a new type, but that isn’t the case. You must still create an object of your new type, and at the point of creation you must specify the exact constructor to use. Thus, if the code that creates objects appears throughout your application, you have the same problem when adding new types. You must still chase down all the places in your code where type matters. Creating the type matters here, not using it (which polymorphism handles). The effect is the same: adding a new type can cause problems.

Encapsulate object creation. You force every object to come from a common factory rather than spreading creational code throughout the system. If your program must go through this factory whenever it needs to create one of your objects, then you change only the factory when you add a new object.

Since every object-oriented program creates objects, and since you will likely extend your program by adding new types, Factory might be the most common design pattern.

Simple Factory Method

As an example, revisit the Shape system. The factory can be a @staticmethod of the base class:

# shapefact1/shape_factory1.py
import random
from collections.abc import Iterator
from typing import override

class Shape:
    def draw(self) -> None: ...
    def erase(self) -> None: ...
    # Create based on class name:
    @staticmethod
    def factory(kind: str) -> Shape:
        match kind:
            case "Circle":
                return Circle()
            case "Square":
                return Square()
            case _:
                raise ValueError(f"Bad shape creation: {kind}")

class Circle(Shape):
    @override
    def draw(self) -> None: print("Circle.draw")
    @override
    def erase(self) -> None: print("Circle.erase")

class Square(Shape):
    @override
    def draw(self) -> None: print("Square.draw")
    @override
    def erase(self) -> None: print("Square.erase")

def shape_name_gen(n: int) -> Iterator[str]:
    for _ in range(n):
        yield random.choice(Shape.__subclasses__()).__name__

if __name__ == "__main__":
    random.seed(4)  # Reproducible shape sequence
    shapes = [Shape.factory(kind) for kind in shape_name_gen(4)]
    for shape in shapes:
        shape.draw()
        shape.erase()
#: Circle.draw
#: Circle.erase
#: Square.draw
#: Square.erase
#: Circle.draw
#: Circle.erase
#: Square.draw
#: Square.erase

The factory() takes an argument that selects the type of Shape to create. Here it is a string, but it could be any kind of data. The factory() is now the only other code in the system that needs to change when you add a new type of Shape (the initialization data for the objects presumably comes from somewhere outside the system, rather than from random generation as in the above example).

I have also used a generator (see Iterators). A factory takes information telling it what to build; a generator object holds an internal algorithm and produces the next value with no argument. shape_name_gen() takes n and returns a generator object, and that object then produces names on demand. Those names are the data driving Shape.factory().

Inside shape_name_gen(), Shape.__subclasses__() produces a list of references to each direct subclass of Shape. It covers only the first level of inheritance, so a class inheriting from Circle does not show up in the list. For a deeper hierarchy, recurse through each subclass’s own __subclasses__().

To discourage direct construction of the concrete shapes, give them module-level names with a leading underscore: a convention rather than concealment, which is as far as Python goes (Singleton makes the same case). Nesting the classes inside factory() looks stronger and is worse. A class statement is executable code, so every call would define fresh Circle and Square classes: two shapes from different calls would share behavior but not a class, failing type(a) is type(b) and isinstance() alike, and Shape.__subclasses__() would no longer name the kinds.

The Pythonic Factory: a Dictionary

A factory turns data, such as a name, into an object without scattering constructors through your code. In Python a class is a first-class object. You can store it in a variable and call it to make an instance.

Thus, the simplest factory is a dictionary that maps names to classes. No factory method and no factory class; the dict is the factory:

# shape_table.py
from typing import Final, override

class Shape:
    def draw(self) -> None: ...

class Circle(Shape):
    @override
    def draw(self) -> None: print("Circle.draw")

class Square(Shape):
    @override
    def draw(self) -> None: print("Square.draw")

SHAPES: Final[dict[str, type[Shape]]] = {
    "Circle": Circle,
    "Square": Square,
}

def make(kind: str) -> Shape:
    return SHAPES[kind]()

make("Circle").draw()
#: Circle.draw
make("Square").draw()
#: Square.draw

The dict values are classes, so type[Shape] is their type, and calling one constructs an instance. Adding a Triangle means one new class and one new line in SHAPES.

You can go one step further, so the factory never needs editing when you add a type, by letting each subclass register itself through __init_subclass__() (see Metaprogramming):

# registry.py
from typing import ClassVar, override

class Shape:
    registry: ClassVar[dict[str, type[Shape]]] = {}

    def __init_subclass__(cls, **kwargs: object) -> None:
        super().__init_subclass__(**kwargs)
        Shape.registry[cls.__name__] = cls

    def draw(self) -> None: ...

class Circle(Shape):
    @override
    def draw(self) -> None: print("Circle.draw")

class Square(Shape):
    @override
    def draw(self) -> None: print("Square.draw")

def make(kind: str) -> Shape:
    return Shape.registry[kind]()

if __name__ == "__main__":
    print(sorted(Shape.registry))
    for kind in ["Circle", "Square", "Circle"]:
        make(kind).draw()
#: ['Circle', 'Square']
#: Circle.draw
#: Square.draw
#: Circle.draw

Nothing in the listing calls a register function. The two class statements fill Shape.registry on their own, as the printed key list shows. Adding a Triangle is now a single class definition. It registers itself, and make() builds it with no change to the factory. Pattern Refactoring uses this same self-registration. Shape.__subclasses__() could have built the table instead, but it stops at direct subclasses; __init_subclass__() runs for every class anywhere below Shape. A dictionary of classes, whether you fill it by hand or the classes fill it themselves, is the ordinary Python factory. That is the dissolution The Pattern Concept describes: the pattern does not go away, it stops needing a class hierarchy to express it. The remaining sections cover the classic object-oriented factories, for contrast.

__init_subclass__() runs as the subclass’s class statement executes. In one file that timing is invisible, but a subclass defined in another module joins the registry only when something imports that module. The classic failure is a plugin that “never registered”: the class is fine, the registry is fine, and nothing imported the module that defines it. A lazy import produces the same failure even when the import statement is in the file. The module body, and with it the registration, waits for the first use of the imported name, and an import written only to trigger registration never uses that name. Running with -X lazy_imports=all does this to ordinary imports too. Import a plugin module eagerly when the import exists for its side effect.

The registry also keys on cls.__name__ alone, so two classes that share a name, from different modules, silently overwrite each other. Key on a qualified name when a collision is possible.

__init_subclass__() names Shape.registry rather than cls.registry on purpose: cls.registry resolves through the MRO, so a subclass that gives itself a registry of its own would quietly start a second table that make() never reads.

The tests confirm that every subclass registers itself, and that a new subclass needs no change to make(). Defining a fresh subclass of Shape inside the test is enough to see it appear in the registry:

# test_registry.py
from typing import override
import pytest
from registry import Circle, Shape, Square, make

def test_subclasses_auto_register() -> None:
    assert Shape.registry["Circle"] is Circle
    assert Shape.registry["Square"] is Square

def test_make_builds_the_right_type() -> None:
    assert isinstance(make("Circle"), Circle)
    assert isinstance(make("Square"), Square)

def test_new_subclass_registers_itself() -> None:
    class Triangle(Shape):
        @override
        def draw(self) -> None: ...

    assert Shape.registry["Triangle"] is Triangle
    assert isinstance(make("Triangle"), Triangle)

def test_unknown_name_raises() -> None:
    with pytest.raises(KeyError):
        make("Hexagon")

Polymorphic Factories

The static factory() method in shape_factory1.py forces all the creation operations into one spot, so that’s the only place you need to change the code. However, GoF Design Patterns emphasizes that the Factory Method pattern exists so you can subclass different types of factories from the basic factory (the above design is a special case). For its sample code, GoF Design Patterns reuses the maze example from the Abstract Factory (the next section covers that pattern), subclassing the game to override its factory methods. This version of shape_factory1.py moves the factory methods into separate classes and calls them polymorphically:

# shapefact2/shape_factory2.py
# Polymorphic factory methods.
import random
from collections.abc import Iterator
from typing import Final, Protocol, override

class ShapeMaker(Protocol):
    def create(self) -> Shape: ...

class Shape:
    def draw(self) -> None: ...
    def erase(self) -> None: ...

class Circle(Shape):
    @override
    def draw(self) -> None: print("Circle.draw")
    @override
    def erase(self) -> None: print("Circle.erase")
    class Factory:
        def create(self) -> Circle: return Circle()

class Square(Shape):
    @override
    def draw(self) -> None: print("Square.draw")
    @override
    def erase(self) -> None: print("Square.erase")
    class Factory:
        def create(self) -> Square: return Square()

FACTORIES: Final[dict[str, ShapeMaker]] = {
    "Circle": Circle.Factory(),
    "Square": Square.Factory(),
}

def create_shape(kind: str) -> Shape:
    return FACTORIES[kind].create()

def shape_name_gen(n: int) -> Iterator[str]:
    types = Shape.__subclasses__()
    for _ in range(n):
        yield random.choice(types).__name__

if __name__ == "__main__":
    random.seed(4)
    shapes = [create_shape(kind) for kind in shape_name_gen(4)]
    for shape in shapes:
        shape.draw()
        shape.erase()
#: Circle.draw
#: Circle.erase
#: Square.draw
#: Square.erase
#: Circle.draw
#: Circle.erase
#: Square.draw
#: Square.erase

Now the factory methods are polymorphic: each type of shape carries its own nested Factory class whose create() method builds an object of that type. FACTORIES maps each kind’s name to an instance of its factory, and create_shape() looks that factory up and calls it right away. A more complex design would hand the factory object back to the caller, who could hold it and create objects from it later. However, much of the time you don’t need the complexity of the polymorphic factory method, and a single static method in the base class (as in shape_factory1.py) works fine.

A Factory class nested in every shape is machinery Python does not need, kept here to show the structure GoF Design Patterns intends; the registry above does the same job with no nested classes. An earlier version of this example did without FACTORIES by dispatching through eval(f"{kind}.Factory()"), which is worse than unnecessary. create_shape() then compiles and runs whatever string it receives, so a kind arriving from a configuration file, a request, or a command line is arbitrary code rather than a shape name. The dictionary lookup either produces a factory or raises a KeyError; exercise 8 stages the attack against both. A separate factory class is worth writing when object creation takes real work beyond calling a constructor, such as pooling, caching, or consulting external configuration.

Abstract Factories

The Abstract Factory pattern looks like the factory objects above, with not one but several factory methods. Each factory method creates a different kind of object. When you create the factory object, you choose the concrete version of every object that factory creates. The example in GoF Design Patterns implements portability across graphical user interfaces (GUIs). You create a factory object appropriate to the GUI that you’re working with, and from then on when you ask it for a menu, button, slider, etc., it automatically creates the appropriate version of that item for the GUI. Thus you can isolate, in one place, the effect of changing from one GUI to another.

As another example, suppose you are creating a general-purpose gaming environment that supports different types of games. Here’s how it might look using an abstract factory:

Two parallel hierarchies, Character and Obstacle, with each concrete factory producing the one matched pair its game needs: KittiesAndPuzzles always pairs Kitty with Puzzle, WarriorsAndWeapons always pairs Warrior with Weapon
# games.py
from typing import override

class Obstacle:
    def action(self) -> str:
        raise NotImplementedError

class Character:
    def interact_with(self, obstacle: Obstacle) -> None:
        raise NotImplementedError

class Kitty(Character):
    @override
    def interact_with(self, obstacle: Obstacle) -> None:
        print("Kitty has encountered a", obstacle.action())

class Warrior(Character):
    @override
    def interact_with(self, obstacle: Obstacle) -> None:
        print("Warrior now battles a", obstacle.action())

class Puzzle(Obstacle):
    @override
    def action(self) -> str:
        return "Puzzle"

class Weapon(Obstacle):
    @override
    def action(self) -> str:
        return "Weapon"

# The Abstract Factory:
class GameElementFactory:
    def make_character(self) -> Character:
        raise NotImplementedError
    def make_obstacle(self) -> Obstacle:
        raise NotImplementedError

# Concrete factories:
class KittiesAndPuzzles(GameElementFactory):
    @override
    def make_character(self) -> Character: return Kitty()
    @override
    def make_obstacle(self) -> Obstacle: return Puzzle()

class WarriorsAndWeapons(GameElementFactory):
    @override
    def make_character(self) -> Character: return Warrior()
    @override
    def make_obstacle(self) -> Obstacle: return Weapon()

class GameEnvironment:
    def __init__(self, factory: GameElementFactory) -> None:
        self.character = factory.make_character()
        self.obstacle = factory.make_obstacle()
    def play(self) -> None:
        self.character.interact_with(self.obstacle)

g1 = GameEnvironment(KittiesAndPuzzles())
g2 = GameEnvironment(WarriorsAndWeapons())
g1.play()
#: Kitty has encountered a Puzzle
g2.play()
#: Warrior now battles a Weapon

In this environment, Character objects interact with Obstacle objects, but the types of characters and obstacles differ depending on what kind of game you’re playing. You determine the kind of game by choosing a particular GameElementFactory, and then the GameEnvironment controls the setup and play of the game. In this example, the setup and play are simple, but those activities (the initial conditions and the state change) can determine much of the game’s outcome. GameEnvironment has no place to vary the rules of play, so a real game would need one, either a subclass overriding play() or a rules object passed alongside the factory.

interact_with() dispatches on the character’s type, and obstacle.action() dispatches again on the obstacle’s, so the pair of calls chooses behavior from both types. Multiple Dispatching develops this into a technique.

The base classes Obstacle, Character, and GameElementFactory (translated from the Java version) force every concrete class to inherit from them. Those raise NotImplementedError bodies enforce less than the listing suggests. They fail at call time: a concrete factory that forgets make_obstacle() constructs without complaint and raises an exception only when the missing method finally runs. An @abstractmethod fails at instantiation, the way Partial() did in Surrogate, which at least catches the omission before anything calls the missing method. Python does not need that inheritance to keep the same checking. A Protocol describes the required shape, and any class with that shape conforms, with no base class to derive from while still type checking:

# games2.py
# Simplified Abstract Factory.
from typing import Protocol

class Obstacle(Protocol):
    def action(self) -> str: ...

class Character(Protocol):
    def interact_with(self, obstacle: Obstacle) -> None: ...

class GameElementFactory(Protocol):
    def make_character(self) -> Character: ...
    def make_obstacle(self) -> Obstacle: ...

class Kitty:
    def interact_with(self, obstacle: Obstacle) -> None:
        print("Kitty has encountered a", obstacle.action())

class Warrior:
    def interact_with(self, obstacle: Obstacle) -> None:
        print("Warrior now battles a", obstacle.action())

class Puzzle:
    def action(self) -> str: return "Puzzle"

class Weapon:
    def action(self) -> str: return "Weapon"

# Concrete factories:
class KittiesAndPuzzles:
    def make_character(self) -> Kitty: return Kitty()
    def make_obstacle(self) -> Puzzle: return Puzzle()

class WarriorsAndWeapons:
    def make_character(self) -> Warrior: return Warrior()
    def make_obstacle(self) -> Weapon: return Weapon()

class GameEnvironment:
    def __init__(self, factory: GameElementFactory) -> None:
        self.character = factory.make_character()
        self.obstacle = factory.make_obstacle()
    def play(self) -> None:
        self.character.interact_with(self.obstacle)

class BrokenFactory:
    def make_character(self) -> Kitty: return Kitty()

g1 = GameEnvironment(KittiesAndPuzzles())
g2 = GameEnvironment(WarriorsAndWeapons())
# GameEnvironment(BrokenFactory())  # ty: invalid-argument-type
g1.play()
#: Kitty has encountered a Puzzle
g2.play()
#: Warrior now battles a Weapon

The concrete classes inherit nothing, but the type checker still verifies that each one fits the appropriate Protocol. BrokenFactory supplies make_character() and forgets make_obstacle(), and uncommenting the line that passes a BrokenFactory to GameEnvironment produces protocol member make_obstacle is not defined on type BrokenFactory. The Protocol catches the omission before the program runs, earlier than either @abstractmethod or raise NotImplementedError can. A GameElementFactory must supply make_character() and make_obstacle(), a Character must supply interact_with(), and an Obstacle must supply action(). This is structural typing from Static Typing. It preserves the purpose of the interfaces, without the coupling a shared base class imposes.

Prototype

The factories so far build each object from a class and some arguments. Prototype takes a different route. It keeps one fully configured instance and makes new objects by copying it. Use it when a ready-made instance is easier to clone than to rebuild, or when construction is expensive and the instances share most of the setup.

The copy module clones any object. copy.deepcopy() follows every reference, so the clone shares no mutable state with the original:

# prototype.py
import copy
from dataclasses import dataclass, field

@dataclass
class Monster:
    name: str
    hp: int
    powers: list[str] = field(default_factory=list)

    def clone(self) -> Monster:
        return copy.deepcopy(self)

goblin = Monster("Goblin", hp=10, powers=["bite"])
# Build a variant by cloning and adjusting, not rebuilding:
captain = goblin.clone()
captain.name = "Goblin Captain"
captain.hp = 20
captain.powers.append("rally")
print(goblin)
#: Monster(name='Goblin', hp=10, powers=['bite'])
print(captain)
#: Monster(name='Goblin Captain', hp=20, powers=['bite', 'rally'])
shallow = copy.copy(goblin)
shallow.powers.append("shared")
print(goblin.powers)  # The original changed too
#: ['bite', 'shared']

captain gets its own powers list, so appending to it leaves goblin.powers unchanged. The clone() method wraps copy.deepcopy(). The last three lines are the trap, not the recommendation: copy.copy() duplicates the Monster and shares its powers list, so changing that list through one object changes it for the other, with no error to signal it.

You can combine prototype with a registry. Instead of a registry of classes, keep a registry of prototypical instances and clone the chosen one:

# prototype_registry.py
import copy
from dataclasses import dataclass, field
from typing import Final

@dataclass
class Monster:
    name: str
    hp: int
    powers: list[str] = field(default_factory=list)

PROTOTYPES: Final[dict[str, Monster]] = {
    "goblin": Monster("Goblin", hp=10, powers=["bite"]),
    "troll": Monster("Troll", hp=40, powers=["smash", "regenerate"]),
}

def spawn(kind: str) -> Monster:
    return copy.deepcopy(PROTOTYPES[kind])

if __name__ == "__main__":
    a = spawn("goblin")
    b = spawn("goblin")
    b.hp = 5
    print(a.hp, b.hp)  # The copies are independent
    print(spawn("troll"))
#: 10 5
#: Monster(name='Troll', hp=40, powers=['smash', 'regenerate'])

spawn() returns an independent object every time, so callers can modify their copy without touching the prototype. Compare this with make() in registry.py. There the table holds classes and calls a constructor. Here it holds instances and copies them. Use the prototype form when the interesting part of an object is its configured state rather than its type.

These tests pin down the two required properties for a prototype registry. Each spawn must be independent, and the stored prototype must never change:

# test_prototype.py
from prototype_registry import PROTOTYPES, spawn

def test_clone_is_independent() -> None:
    a = spawn("goblin")
    b = spawn("goblin")
    b.powers.append("curse")
    assert a.powers == ["bite"]
    assert b.powers == ["bite", "curse"]

def test_prototype_untouched() -> None:
    spawned = spawn("troll")
    spawned.hp = 1
    assert PROTOTYPES["troll"].hp == 40

Builder

Builder is the last of the GoF Design Patterns creational patterns left to cover (Singleton has its own chapter): separate the construction of a complex object from its representation, assembling it in steps. In Java and C++ it cures the telescoping constructor. A class with many optional settings needs a constructor for every useful combination, because those languages have no keyword arguments. The workaround is a companion class that collects settings one method call at a time. Translated directly into Python, it looks like this:

# pizza_builder.py
from dataclasses import dataclass
from typing import Self

@dataclass(frozen=True)
class Pizza:
    size: int
    cheese: bool
    toppings: tuple[str, ...]

class PizzaBuilder:
    def __init__(self) -> None:
        self._size = 12
        self._cheese = True
        self._toppings: list[str] = []

    def size(self, inches: int) -> Self:
        self._size = inches
        return self

    def no_cheese(self) -> Self:
        self._cheese = False
        return self

    def topping(self, name: str) -> Self:
        self._toppings.append(name)
        return self

    def build(self) -> Pizza:
        return Pizza(
            self._size, self._cheese, tuple(self._toppings))

if __name__ == "__main__":
    pizza = (PizzaBuilder()
             .size(16)
             .topping("basil")
             .topping("olives")
             .build())
    print(pizza)
#: Pizza(size=16, cheese=True, toppings=('basil', 'olives'))

Each setter returns self, annotated with Self from Static Typing, so the calls chain. build() freezes the accumulated settings into an immutable Pizza. The class works and reads well, but it solves a problem Python does not have. Keyword arguments with defaults are the built-in builder:

# pizza_direct.py
from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Pizza:
    size: int = 12
    cheese: bool = True
    toppings: tuple[str, ...] = ()

if __name__ == "__main__":
    pizza = Pizza(size=16, toppings=("basil", "olives"))
    print(pizza)
    family = replace(pizza, size=20)
    print(family)
#: Pizza(size=16, cheese=True, toppings=('basil', 'olives'))
#: Pizza(size=20, cheese=True, toppings=('basil', 'olives'))

Every combination of settings is a single call, the call site names each option just as the chain does, and the defaults live on the fields instead of inside a second class. dataclasses.replace() covers the other use of builder chains: starting from an existing configuration and varying it. For a frozen data class, replace() is Prototype and Builder rolled into one function, copying the configured state and changing chosen fields on the way. copy.replace() is the general form of the same operation, working on any object that defines __replace__(); a data class defines that method for you. A test confirms the two forms produce the same pizza:

# test_pizza.py
from dataclasses import replace
import pizza_builder as pb
import pizza_direct as pd

def test_builder_and_keywords_agree() -> None:
    built = (pb.PizzaBuilder()
             .size(16).topping("basil").build())
    direct = pd.Pizza(size=16, toppings=("basil",))
    assert (built.size, built.cheese, built.toppings) == (
        direct.size, direct.cheese, direct.toppings)

def test_replace_varies_one_field() -> None:
    base = pd.Pizza()
    variant = replace(base, size=18)
    assert base.size == 12 and variant.size == 18
    assert variant.toppings == base.toppings

Decorators has its own Pizza, modeling toppings as wrapper objects instead of builder-collected fields, to illustrate the unrelated Decorator pattern.

Builder survives in Python when construction is genuinely a process. The steps must come in an order, later steps depend on earlier ones, and rules span the steps. GameBuilder in Simulation qualifies. It assembles a maze in three stages, creating rooms, connecting doors, then pairing the teleports that share a target letter, and each stage relies on what the previous stage established. No single constructor call can express that. The standard library’s argparse.ArgumentParser has the same shape. add_argument() calls accumulate a specification, and parse_args() is the build().

The humblest builder in Python is easy to overlook. Appending parts to a list and finishing with "".join(parts) builds an immutable product, a string, through a mutable intermediate. That is the Builder structure. PizzaBuilder collecting toppings in a list and freezing them into a tuple at build() is the same move. Reserve the pattern, and the name, for construction that is a process with intermediate state and rules of its own. When the “steps” are optional values, keyword arguments and a data class are the builder.

Which Factory to Use

Match the machinery to what varies:

The static factory() method and the nested-Factory-class dispatcher are here because GoF Design Patterns describes them, not because Python needs them. Both exist to work around languages where a class is not an object you can put in a dictionary.

Exercises

  1. Add a class Triangle to shape_factory1.py.
  2. Add a class Triangle to shape_factory2.py.
  3. Add a new type of GameElementFactory called GnomesAndFairies, first to games.py and then to games2.py. In games2.py, leave out make_obstacle() at first and confirm the error your type checker reports; then add it.
  4. Modify shape_factory2.py so that it uses an Abstract Factory to create different sets of shapes (for example, one particular type of factory object creates “thick shapes,” another creates “thin shapes,” but each factory object can create all the shapes: circles, squares, triangles etc.).
  5. Add a rule to both pizza examples: a pizza may carry at most four toppings. In pizza_direct.py, enforce it with __post_init__(). In pizza_builder.py, decide whether it belongs in topping() or build(). In which version can an invalid pizza exist, even momentarily?
  6. Move Circle and Square out of registry.py into a new module, extra_shapes.py. Confirm that make("Circle") now raises KeyError until something imports extra_shapes, and explain which line of which file registers the class, and when it runs.
  7. Give Monster in prototype_registry.py a parts: dict[str, int] field and add a prototype that uses it. Change spawn() to use copy.copy() instead of copy.deepcopy(), run test_prototype.py, and explain which assertion fails and why. Then restore deepcopy() and add a test that would have caught the bug through parts rather than powers.
  8. Recreate the eval() dispatcher described after shape_factory2.py’s listing: a create_shape() that builds each factory with eval(f"{kind}.Factory()") instead of consulting FACTORIES. Call it with a kind string that is not a shape name but a Python expression with a side effect, and show that it runs the expression. Then show that the FACTORIES version raises KeyError for the same string.