Embercode
✕
Object-Oriented ProgrammingLesson 14 of 17

40 min read

Factory methods and builders in Python

Why this matters

Every object starts with a call to its class. For a small class with two or three attributes, __init__ is enough. As a class grows, the call becomes hard to read. A call such as Server("example.com", 443, 30, True) does not say what the values mean. A reader has to open the class to find out. Worse, Python accepts two numbers in the wrong order without complaint, because both are integers.

Python has a feature that solves most of this: keyword arguments. Two patterns solve the rest. A class method used as a factory is a method attached to the class that creates an object and has a name that explains the creation. A builder is a separate helper object that collects the values one at a time, checks them, and then creates the object. This lesson covers both, with one example for each, and a short comparison.

The lesson assumes you know classes, __init__, and attributes that start with an underscore. The previous lesson in this module covered exceptions, and this lesson uses ValueError for bad input. Immutable objects are covered in an earlier lesson of this module.

The problem with many positional values

A positional argument is a value matched to a parameter by its place in the call. Here is a small class whose initializer takes four values, with defaults for the last two:

Python · editable

The output is:

Server(example.com, 30, timeout=443, tls=False)
Server(example.com, 443, timeout=30, tls=True)

The first call was meant to use port 443 and timeout 30. It used port 30 and timeout 443, and Python accepted it without a warning. This is the weakness of positional values, because both numbers are integers, and the call looks fine.

The second call uses keyword arguments. Each value is named in the call, as in port=443, so the order no longer matters. The name also explains the value to the reader. Use keyword arguments for any call where two values could be mixed up. A parameter can be forced to be keyword-only by writing * before it, as in def __init__(self, host, *, port, timeout=30). Then a positional call such as Server("example.com", 443) raises an error.

The keyword form solves the naming problem for a small class. For a class with many optional values, or where a creation has a meaning of its own, the two patterns below are better.

Class method used as a factory

A factory method is a method that creates and returns an object. In Python the usual form is a class method, marked with @classmethod. It receives the class as its first argument, named cls, and it returns cls(...). You call it on the class name, with no object needed first.

The idea has two parts. The initializer takes the raw values. The factory method has a name that says what the creation means, and it checks the input before it calls the initializer.

Here is a Color class with two factory methods. Each one creates a color in a different way:

Python · editable

The output is:

Color(255, 136, 0)
#ff8800
Color(255, 136, 0)
Error: red must be 0 to 255, got 300
Error: expected a form like #rrggbb, got: ff8800

Three things in this code are worth reading closely.

The first is the name. Color.rgb(255, 136, 0) says that the numbers are red, green and blue. Color.from_hex("#ff8800") says that the text is a hex code. The initializer could not say either of these things. Python's keyword arguments help only when the caller writes them. The method name does that job for every caller.

The second is the validation. Every way into the class goes through a check, so a Color with a red part of 300 cannot be made through the factory. This is the same invariant idea from the earlier lessons of this module: the class protects its own rules, and the factory is the gate. Python does not stop a caller from using Color(300, 0, 0) directly. The convention is to call the factory methods, and to keep the initializer for the factories to use.

The third is the cls(...) call. Using cls instead of the class name means a subclass that calls the factory gets an object of its own class. A class method that writes Color(...) would always return a plain Color.

Shared objects from a factory

A factory can also return a shared object. The common example is a fixed set of named values, such as WHITE, that should be the same object every time they are used. Python can store such a value as a class attribute.

Python · editable

The output is:

True
Color(0, 0, 0)

The assignments come after the class body, because the class does not exist yet while its body runs. Each use of Color.WHITE gives the same object, so no new object is made. A plain call to Color(...) always creates a new object. A factory can return a stored object or a shared object. Callers cannot tell the difference, and they do not need to.

A factory can return a different class

A factory can also return an interface-like object and hide the class that does the work. The caller uses the common method, send. The factory decides which class to create, based on the input. This keeps callers simple, and it lets you add a new kind later without changing them.

Python · editable

The output is:

email to a@example.com: build passed
sms to +91 00000 00000: build passed
EmailNotifier
Error: unknown channel: pigeon

The match statement chooses the class for each channel name. The case _ line handles any other name by raising an error, so the function never returns None by accident. The caller calls send and does not know, or care, which class answered. The expression type(n1).__name__ prints the real class, and it shows that the factory chose EmailNotifier for the email config.

Builder for many optional fields

A builder is a helper class whose only job is to collect values and then build the real object. The caller writes a chain of method calls, one for each value it wants to set. Each method returns the builder itself, so the next call can follow on the same line. The last call is build(). That method checks the rules and creates the object.

Builders suit objects with several optional fields, where most callers set only a few. A pizza order is a good first example. Only the size is required. The crust, the cheese and the toppings are optional, and each has a default.

Python · editable

The output is:

Pizza(size=large, crust=regular, extra_cheese=False, toppings=[])
Pizza(size=medium, crust=thin, extra_cheese=True, toppings=['mushroom', 'olive'])
Pizza(size=small, crust=regular, extra_cheese=False, toppings=['onion'])
Pizza(size=small, crust=regular, extra_cheese=False, toppings=['onion', 'pepper'])
Error: size is required
Error: at most 5 toppings, got 6

Read the code in this order.

The Pizza initializer takes the builder as its only argument. The builder is the only thing that creates a pizza in normal use, so every pizza goes through build(). The names that start with an underscore, such as _toppings, are internal to the builder and the pizza. The pizza reads them once and stores its own values.

The required field, size, is checked in the builder's initializer. The optional fields have default values set in the initializer: _crust starts as "regular" and _extra_cheese starts as False. The caller only writes the calls it needs.

The rule about toppings is checked in build(), not in each method. Checking in one place means the rule is easy to find and easy to change. It also lets a rule depend on several values at once, which a single method cannot do.

The output shows a second benefit of the builder, and a subtle point. The name shared is used for two pizzas. The first pizza has only onion, even though pepper was added afterwards. This works because the pizza copies the list into a tuple in its initializer, with tuple(builder._toppings). A tuple cannot be changed, and the copy is separate from the builder's list. Without that copy, both pizzas would share one list, and the second topping call would change the first pizza. Copying the mutable list, which can be changed after it is made, is the step that makes the built object safe.

A longer example: a request with rules that depend on each other

The second example is harder. It builds an HTTP request, where some values have rules that depend on each other. A request body is allowed only with a POST or PUT. The timeout must be positive. The headers come as name and value pairs, and each name must not be blank.

Python · editable

The output is:

GET https://example.com/status timeout=30s headers={'Accept': 'application/json'} body=none
POST https://example.com/items timeout=10s headers={'Content-Type': 'application/json'} body="{"name":"tea"}"
Error: GET requests cannot have a body
Error: url must start with https://, got: http://example.com

This example shows three rules working together. The URL is checked as soon as the builder is created, because a bad URL is never useful. The timeout and header name are checked in their own methods, because each one only depends on its own value. The rule that links the method and the body is checked in build(), because it needs two values at once. Put each rule where the information it needs is available.

The request copies the headers dictionary in its initializer with dict(...). A plain assignment would share the dictionary with the builder, and a later header call would change a request that was already built. The copy keeps each request separate. Python dictionaries keep their insertion order, so the headers print in the order they were added.

Factory or builder?

Both patterns create objects, and they can be combined. The choice depends on the number of values and on whether the creation has a meaning of its own.

SituationBetter choice
Two or three required values, no optional onesThe initializer, with keyword arguments when the names help
Creation has a meaning worth naming, such as parsing or a fixed valueA class method used as a factory
Many optional values, most callers set only a fewA builder
The caller should use a common method and not know the classA factory that returns one of several classes

The table is a guide. A class can have a class method that starts a builder, such as Pizza.builder("large"). Both patterns can be combined this way.

Common mistakes

Most mistakes in this area come from one of three habits: skipping validation, sharing mutable data, and forgetting that a builder method must return the builder.

Forgetting to return the builder

A method in a builder must return self. If it returns nothing, the value of the call is None, and the next call in the chain fails. Here is a builder whose set_crust method forgets to return:

Python · editable

The output is:

None
AttributeError: 'NoneType' object has no attribute 'build'

The first line shows that the variable b holds None, not a builder. The second error comes from the chain: set_crust returned None, so .build() was called on nothing. The fix is to add return self at the end of the method.

Skipping validation

A builder with no checks produces objects that break their own rules. The Server example earlier had no check on the port number, and nothing caught the mistake. A builder gives you a single place to check, in build(). Put the checks there, or in the method if they depend only on that value.

Sharing a mutable list between built objects

The Pizza example copied the list into a tuple in its initializer. Without that copy, a builder that is reused would change every object it built before. In the shared example, the first pizza would also print pepper.

Using a builder for two values

A builder adds code. For a class with two values, an initializer with keyword arguments is shorter and just as clear. Choose a builder when the number of optional values makes the call hard to read.

Using a positional call where a name would help

A Server("example.com", 30, 443) call is hard to read, and it hides a mistake. Server(host="example.com", port=443, timeout=30) reads like a sentence. If a creation has a meaning, give it a name with a factory, or name the values in the call.

Practice

Each exercise is followed by its answer. Try to answer before reading.

Exercise 1

Write a class method Temperature.from_celsius(value) for a class whose initializer stores the value. The method must reject values below absolute zero, which is -273.15 degrees Celsius, with a ValueError.

Show the answer

Python · editable

The output is 25.0, then Error: below absolute zero: -300.0. The class method is the only path that checks the value, and the name from_celsius tells the reader which unit the number uses.

Exercise 2

A Car builder has a required make, and optional color (default "white"), doors (default 4) and sunroof (default False). Write the build method so that a car with doors below 2 or above 6 is rejected with a ValueError.

Show the answer

def build(self):
    if not 2 <= self._doors <= 6:
        raise ValueError(f"doors must be 2 to 6, got {self._doors}")
    return Car(self)

The check is in build, not in the doors method, so it runs once and can use any other value if needed. The caller still gets an error before a car is made.

Exercise 3

Explain in two sentences why a factory can return the shared value Color.WHITE every time, but a call to the class always creates a new object.

Show the answer

A call to the class always runs __init__ on a new object. A class method has no such rule, so it can return an object that already exists, such as a value stored on the class. Callers cannot tell the difference, because they only receive a Color.

Exercise 4

A builder stores a list of toppings, and its build method passes the same list to the new object without copying. What goes wrong if the builder is used twice?

Show the answer

Both built objects point to the same list. If the caller adds a value to the builder after the first build(), the first object changes too. The fix is to copy the list in the object's initializer, with list(...), or to store a tuple, which cannot be changed.

Exercise 5

Write one sentence that says when you would choose a factory method over a builder.

Show the answer

Choose a factory method when the creation has a name worth giving, or when callers should get a common method without knowing the class, and the object has few values to set. Choose a builder when many values are optional and most callers set only a few.

Summary

  • A call with several values of the same type is easy to get wrong. Keyword arguments name each value in the call, so the order no longer matters.
  • A class method used as a factory has a name that explains the creation. It checks the input before it calls the initializer, and it uses cls(...), so subclasses get their own type.
  • A factory can return a shared object, or one of several classes, and hide the choice from the caller.
  • A builder collects optional values one at a time, with each method returning self, and it checks the rules in build().
  • The built object should copy any mutable collection it receives, so later changes to the builder do not reach it.
Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Exceptions in class design in Python