Skip to main content
Low-Level Design · lesson 18 of 18
Open the Low-Level Design page →
Low-Level Design advanced Lesson 18 of 18

Abstract Factory and Prototype in Python: Matching Families and Cloning Objects

Fix a dark dialog paired with a light button with one Abstract Factory per theme, then clone costly objects with Prototype and avoid shallow copies.

By SkillByExample · Published

lld design-patterns python abstract-factory prototype copy

Two creational patterns solve two different problems: making sure objects that belong together are created together, and creating many similar objects without paying the full setup cost each time.

Widgets from two themes, mixed by accident

Each screen creates its widget classes directly. One was copied from an older screen.

# themes_mixed.py: screens create widget classes directly, and one picks the wrong theme
class LightButton:
    def render(self): return "[ OK ] (light)"
class DarkButton:
    def render(self): return "[ OK ] (dark)"
class LightDialog:
    def render(self): return "| Save changes? | (light)"
class DarkDialog:
    def render(self): return "| Save changes? | (dark)"

def settings_screen():                        # written for the dark theme
    return [DarkDialog(), DarkButton()]

def save_prompt():                            # also meant to be dark, copied from an old screen
    return [DarkDialog(), LightButton()]

for name, screen in [("settings", settings_screen()), ("save prompt", save_prompt())]:
    print(f"{name}:", " ".join(w.render() for w in screen))
$ python themes_mixed.py
settings: | Save changes? | (dark) [ OK ] (dark)
save prompt: | Save changes? | (dark) [ OK ] (light)

The save prompt shows a dark dialog with a light button. Nothing failed, because both classes have a render() method; the mistake is only visible on screen. Every function that names widget classes is another place to pick the wrong one.

Abstract Factory: one factory per family

A factory has a creation method for each widget, and each theme is its own factory. Screens ask the factory for widgets and never name a class.

# abstract_factory.py: one factory per theme creates every widget, so a screen can't mix them
from abc import ABC, abstractmethod

class LightButton:
    def render(self): return "[ OK ] (light)"
class DarkButton:
    def render(self): return "[ OK ] (dark)"
class LightDialog:
    def render(self): return "| Save changes? | (light)"
class DarkDialog:
    def render(self): return "| Save changes? | (dark)"

class ThemeFactory(ABC):
    @abstractmethod
    def button(self): ...
    @abstractmethod
    def dialog(self): ...

class LightTheme(ThemeFactory):
    def button(self): return LightButton()
    def dialog(self): return LightDialog()

class DarkTheme(ThemeFactory):
    def button(self): return DarkButton()
    def dialog(self): return DarkDialog()

def save_prompt(theme):                       # asks the factory; never names a widget class
    return [theme.dialog(), theme.button()]

for theme in (LightTheme(), DarkTheme()):
    print(f"{type(theme).__name__}:", " ".join(w.render() for w in save_prompt(theme)))
$ python abstract_factory.py
LightTheme: | Save changes? | (light) [ OK ] (light)
DarkTheme: | Save changes? | (dark) [ OK ] (dark)

save_prompt now takes a theme and calls theme.dialog() and theme.button(). Whichever factory it gets, the two widgets come from the same family, so the mix-up can’t happen. Switching the whole app to dark mode is passing a different factory. The abstract base class also makes Python refuse a new theme that forgets one of the widgets.

The cost is a factory class per family and a method per product. If an app has one theme, plain constructors are fine. If it only needs to vary one product, a factory method is enough.

Prototype: build once, then clone

Some objects are expensive to construct. Prototype builds one fully configured instance, then copies it.

# prototype_costly.py: building a report template is slow; cloning a configured one is not
import copy, time

class ReportTemplate:
    def __init__(self):
        time.sleep(0.2)                       # stands in for loading fonts and a style sheet
        self.styles = {f"style{i}": {"size": 10 + i % 4} for i in range(50)}
        self.sections = ["Summary", "Details"]
        self.title = "Untitled"

start = time.perf_counter()
built = [ReportTemplate() for month in ["Jan", "Feb", "Mar"]]
print(f"3 built from scratch:     {time.perf_counter() - start:.1f}s")

start = time.perf_counter()
prototype = ReportTemplate()                  # pay the setup once
reports = []
for month in ["Jan", "Feb", "Mar"]:
    r = copy.deepcopy(prototype)              # a clone skips __init__ entirely
    r.title = f"Sales {month}"
    reports.append(r)
print(f"1 built + 3 clones:       {time.perf_counter() - start:.1f}s")
print("titles:", [r.title for r in reports])
$ python prototype_costly.py
3 built from scratch:     0.6s
1 built + 3 clones:       0.2s
titles: ['Sales Jan', 'Sales Feb', 'Sales Mar']

Building three templates paid the 0.2-second setup three times. Building one and cloning it paid it once, because copy.deepcopy doesn’t call __init__; each clone only needed its title changed. Cloning has a cost too: deepcopy still copies the 50 style entries per report, which is why it only wins when the setup does slow work the copy can skip, such as reading files or querying a database. For a constructor that only builds a dict, just call it.

The shallow-copy trap

The clone has to be independent of the prototype. copy.copy isn’t.

# prototype_shallow.py: a shallow copy shares the list inside, so the clone edits the original
import copy

class ReportTemplate:
    def __init__(self):
        self.sections = ["Summary", "Details"]

original = ReportTemplate()
shallow = copy.copy(original)
shallow.sections.append("Appendix")           # meant for this one report only
print("shallow clone:", shallow.sections)
print("original:     ", original.sections, "<- changed too")
print("same list object?", shallow.sections is original.sections)

original = ReportTemplate()
deep = copy.deepcopy(original)
deep.sections.append("Appendix")
print("deep clone:   ", deep.sections)
print("original:     ", original.sections)
print("same list object?", deep.sections is original.sections)
$ python prototype_shallow.py
shallow clone: ['Summary', 'Details', 'Appendix']
original:      ['Summary', 'Details', 'Appendix'] <- changed too
same list object? True
deep clone:    ['Summary', 'Details', 'Appendix']
original:      ['Summary', 'Details']
same list object? False

copy.copy created a new ReportTemplate, but its sections attribute is the same list object as the original’s, so appending through the clone changed both. copy.deepcopy copies the list too. Any prototype holding lists, dicts or other mutable objects needs deepcopy, or a custom __deepcopy__ (Practice 3).

Practice

1. Add a high-contrast theme. How much of save_prompt has to change?
# p23_1.py: a high-contrast theme is one new factory; save_prompt doesn't change
class HighContrastButton:
    def render(self): return "[[ OK ]] (high contrast)"
class HighContrastDialog:
    def render(self): return "|| Save changes? || (high contrast)"

class HighContrastTheme:
    def button(self): return HighContrastButton()
    def dialog(self): return HighContrastDialog()

def save_prompt(theme):
    return [theme.dialog(), theme.button()]

print(" ".join(w.render() for w in save_prompt(HighContrastTheme())))
$ python p23_1.py
|| Save changes? || (high contrast) [[ OK ]] (high contrast)

None of it. A new family is one factory class plus its widgets; every screen that takes a theme gets the new look without any edits. (Duck typing means this short version doesn’t need the ABC, but keeping it would catch a missing method.)

2. Keep a registry of named email templates, and write clone(name, **changes). Check that changing a clone leaves the registry alone.
# p23_2.py: a registry of named email templates; clone() hands out independent copies
import copy

class EmailTemplate:
    def __init__(self, subject, paragraphs):
        self.subject, self.paragraphs = subject, paragraphs

registry = {
    "welcome": EmailTemplate("Welcome aboard", ["Thanks for signing up.", "Here's how to start."]),
    "receipt": EmailTemplate("Your receipt", ["Payment received."]),
}

def clone(name, **changes):
    email = copy.deepcopy(registry[name])
    for field, value in changes.items():
        setattr(email, field, value)
    return email

a = clone("welcome")
b = clone("welcome", subject="Welcome back")
b.paragraphs.append("We missed you.")
for label, e in [("a", a), ("b", b), ("registry", registry["welcome"])]:
    print(f"{label:>8}: {e.subject!r}, {len(e.paragraphs)} paragraphs")
$ python p23_2.py
       a: 'Welcome aboard', 2 paragraphs
       b: 'Welcome back', 3 paragraphs
registry: 'Welcome aboard', 2 paragraphs

Clone b got a new subject and a third paragraph; a and the registry’s template still have the original subject and two paragraphs. deepcopy made the paragraphs list independent for every clone.

3. Give each cloned ticket its own id by defining __deepcopy__.
# p23_3.py: __deepcopy__ lets a clone get a fresh id instead of copying the prototype's
import copy, itertools

ids = itertools.count(1)

class Ticket:
    def __init__(self, queue):
        self.id, self.queue, self.tags = next(ids), queue, ["new"]
    def __deepcopy__(self, memo):
        clone = Ticket.__new__(Ticket)
        memo[id(self)] = clone                # register first, so cycles back to self work
        clone.id = next(ids)                  # every ticket needs its own id
        clone.queue = self.queue
        clone.tags = copy.deepcopy(self.tags, memo)
        return clone

template = Ticket("support")
t1, t2 = copy.deepcopy(template), copy.deepcopy(template)
print([(t.id, t.queue, t.tags) for t in (template, t1, t2)])
$ python p23_3.py
[(1, 'support', ['new']), (2, 'support', ['new']), (3, 'support', ['new'])]

Without the custom method, deepcopy would copy id too and every clone would be ticket 1. __deepcopy__ runs instead of the default, assigns a new id, and still deep-copies tags with memo. Registering the clone in memo before copying the children is what keeps shared and recursive references correct: if tags ever pointed back at the ticket, the copy would reuse the clone instead of recursing forever.

Frequently Asked Questions

What is the Abstract Factory pattern?
Abstract Factory is an object with one creation method per product in a family, such as button() and dialog() for a UI theme. Each concrete factory, like LightTheme or DarkTheme, creates a matching set. Code that builds screens takes a factory and never names a widget class, so it can't mix products from different families.
What is the difference between Factory Method and Abstract Factory?
A factory method creates one kind of object and lets subclasses choose which class. An Abstract Factory groups several related creation methods, so a whole family of objects is chosen together. If you only ever create one product type, a factory method or even a plain function is enough.
What is the Prototype pattern in Python?
Prototype creates new objects by copying an existing, already configured one instead of building each from scratch. In Python that is usually copy.deepcopy on a prototype instance. It helps when construction is expensive, such as loading data or parsing configuration, and the copies only differ in a few fields.
What is the difference between copy.copy and copy.deepcopy?
copy.copy makes a new outer object, but its attributes still point at the same lists and dicts as the original, so changing a list through the copy changes the original too. copy.deepcopy copies the nested objects as well, so the clone is fully independent. Use deepcopy for prototypes that contain mutable containers.
How do I customise what copy.deepcopy does?
Define a __deepcopy__(self, memo) method on the class. It runs instead of the default copying, so it can, for example, give each clone a fresh id while deep-copying the rest. Register the new object with memo[id(self)] = clone before copying the children, and pass memo to the nested deepcopy calls; that keeps shared and recursive references correct.