7.3. Factory Method
EN: Factory Method
PL: Metoda wytwórcza
Type: class
The Factory Method design pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
Here's a simple example of the Factory Method pattern in Python:
>>> class Creator:
... def factory_method(self):
... pass
...
... def some_operation(self):
... product = self.factory_method()
... result = f"Creator: The same creator's code has just worked with {product.operation()}"
... return result
...
>>> class ConcreteCreator1(Creator):
... def factory_method(self):
... return ConcreteProduct1()
...
>>> class ConcreteCreator2(Creator):
... def factory_method(self):
... return ConcreteProduct2()
...
>>> class Product:
... def operation(self):
... pass
...
>>> class ConcreteProduct1(Product):
... def operation(self):
... return "{Result of the ConcreteProduct1}"
...
>>> class ConcreteProduct2(Product):
... def operation(self):
... return "{Result of the ConcreteProduct2}"
...
>>> creator1 = ConcreteCreator1()
>>> creator1.some_operation()
"Creator: The same creator's code has just worked with {Result of the ConcreteProduct1}"
>>> creator2 = ConcreteCreator2()
>>> creator2.some_operation()
"Creator: The same creator's code has just worked with {Result of the ConcreteProduct2}"
In this example, Creator is an abstract class that declares the factory method that returns new product objects. ConcreteCreator1 and ConcreteCreator2 are concrete classes that implement the factory method to produce products of ConcreteProduct1 and ConcreteProduct2 types respectively. Product is an interface for the products, and ConcreteProduct1 and ConcreteProduct2 are concrete products that implement the Product interface.
7.3.1. Pattern
Defer the creation of an object to subclasses
Relays on inheritance and polymorphism
Adds flexibility to the design
7.3.2. Problem
Tightly coupled with MatchaEngine
What if we have better templating engine
class PDF:
def render(self):
return 'PDF'
class Word:
def render(self):
return 'Word'
if __name__ == '__main__':
filename = 'myfile.pdf'
if filename.endswith('.pdf'):
file = PDF(filename)
elif filename.endswith('.docx'):
file = Word(filename)
type(file)
# <class '__main__.PDF'>
7.3.3. Solution
class PDF:
def render(self):
return 'PDF'
class Word:
def render(self):
return 'Word'
def document(filename):
if filename.endswith('.pdf'):
return PDF()
elif filename.endswith('.docx'):
return Word()
else:
raise ValueError('Unknown format')
if __name__ == '__main__':
file = document('myfile.pdf')
type(file)
# <class '__main__.PDF'>
class PDF:
def render(self):
return 'PDF'
class Word:
def render(self):
return 'Word'
class Document:
def __new__(cls, filename):
if filename.endswith('.pdf'):
return PDF()
elif filename.endswith('.docx'):
return Word()
else:
raise ValueError('Unknown format')
if __name__ == '__main__':
file = Document('myfile.pdf')
type(file)
# <class '__main__.PDF'>
7.3.4. Use Case - 1
class PDF:
def render(self):
return 'PDF'
class Word:
def render(self):
return 'Word'
def document(filename):
if filename.endswith('.pdf'):
return PDF()
elif filename.endswith('.docx'):
return Word()
else:
raise ValueError('Unknown format')
if __name__ == '__main__':
a = document('file.pdf')
b = document('file.docx')
type(a)
# <class '__main__.PDF'>
type(b)
# <class '__main__.Word'>
7.3.5. Use Case - 2
class PDF:
def render(self):
return 'PDF'
class Word:
def render(self):
return 'Word'
class Document:
def __new__(cls, filename):
if filename.endswith('.pdf'):
return PDF()
elif filename.endswith('.docx'):
return Word()
else:
raise ValueError('Unknown format')
if __name__ == '__main__':
a = Document('file.pdf')
b = Document('file.docx')
type(a)
# <class '__main__.PDF'>
type(b)
# <class '__main__.Word'>
7.3.6. Use Case - 3
from abc import ABC, abstractmethod
class Document(ABC):
@property
@abstractmethod
def extension(self):
return
def __new__(cls, filename):
name, extension = filename.split('.')
plugins = cls.__subclasses__()
for plugin in plugins:
if plugin == extension:
instance = object.__new__(cls)
instance.__init__()
return instance
else:
raise NotImplementedError('File format unknown')
class PDF(Document):
extension = 'pdf'
class Txt(Document):
extension = 'txt'
class Word(Document):
extension = 'docx'
if __name__ == '__main__':
file = Document('myfile.txt')
print(type(file))
# <class '__main__.Txt'>
file = Document('myfile.pdf')
print(type(file))
# <class '__main__.PDF'>
7.3.7. Use Case - 4
class Setosa:
pass
class Versicolor:
pass
class Virginica:
pass
def iris_factory(species):
if species == 'setosa':
return Setosa
elif species == 'versicolor':
return Versicolor
elif species == 'virginica':
return Virginica
else:
raise NotImplementedError
if __name__ == '__main__':
iris = iris_factory('setosa')
print(iris)
# <class '__main__.Setosa'>
iris = iris_factory('virginica')
print(iris)
# <class '__main__.Virginica'>
iris = iris_factory('arctica')
# Traceback (most recent call last):
# NotImplementedError
7.3.8. Use Case - 5
class Setosa:
pass
class Virginica:
pass
class Versicolor:
pass
def iris_factory(species):
try:
classname = species.capitalize()
cls = globals()[classname]
return cls
except KeyError:
raise NotImplementedError
if __name__ == '__main__':
iris = iris_factory('setosa')
print(iris)
# <class '__main__.Setosa'>
iris = iris_factory('virginica')
print(iris)
# <class '__main__.Virginica'>
iris = iris_factory('arctica')
# Traceback (most recent call last):
# NotImplementedError
7.3.9. Use Case - 6
from abc import ABC, abstractproperty, abstractmethod
from dataclasses import dataclass
@dataclass
class ConfigParser(ABC):
filename: str
@abstractproperty
@property
def extension(self):
pass
def show(self):
content = self.read()
return self.parse(content)
@abstractmethod
def parse(self, content: str) -> dict:
return NotImplementedError
def read(self):
with open(self.filename) as file:
return file.read()
def __new__(cls, filename, *args, **kwargs):
_, extension = filename.split('.')
for parser in cls.__subclasses__():
if parser.extension == extension:
instance = super().__new__(parser)
instance.__init__(filename)
return instance
else:
raise NotImplementedError('Parser for given file type not found')
class ConfigParserINI(ConfigParser):
extension = 'ini'
def parse(self, content: str) -> dict:
print('Parsing INI file')
class ConfigParserCSV(ConfigParser):
extension = 'csv'
def parse(self, content: str) -> dict:
print('Parsing CSV file')
class ConfigParserYAML(ConfigParser):
extension = 'yaml'
def parse(self, content: str) -> dict:
print('Parsing YAML file')
class ConfigFileJSON(ConfigParser):
extension = 'json'
def parse(self, content: str) -> dict:
print('Parsing JSON file')
class ConfigFileXML(ConfigParser):
extension = 'xml'
def parse(self, content: str) -> dict:
print('Parsing XML file')
if __name__ == '__main__':
# iris.csv or *.csv, *.json *.yaml...
# filename = input('Type filename: ')
config = ConfigParser('/tmp/myfile.json')
config.show()
7.3.10. Use Case - 7
import os
class HttpClientInterface:
def GET(self):
raise NotImplementedError
def POST(self):
raise NotImplementedError
class GatewayLive(HttpClientInterface):
def GET(self):
print('Execute GET request over network')
return ...
def POST(self):
print('Execute POST request over network')
return ...
class GatewayStub(HttpClientInterface):
def GET(self):
print('Returning stub GET')
return {'firstname': 'Mark', 'lastname': 'Watney'}
def POST(self):
print('Returning stub POST')
return {'status': 200, 'reason': 'OK'}
class HttpGatewayFactory:
def __new__(cls, *args, **kwargs):
if os.getenv('ENVIRONMENT') == 'production':
return GatewayLive()
else:
return GatewayStub()
if __name__ == '__main__':
os.environ['ENVIRONMENT'] = 'testing'
client = HttpGatewayFactory()
result = client.GET()
# Returning stub GET
result = client.POST()
# Returning stub POST
os.environ['ENVIRONMENT'] = 'production'
client = HttpGatewayFactory()
result = client.GET()
# Execute GET request over network
result = client.POST()
# Execute POST request over network
7.3.11. Use Case - 8
from abc import ABC, abstractmethod
class Path(ABC):
def __new__(cls, path, *args, **kwargs):
if path.startswith(r'C:\Users'):
instance = object.__new__(WindowsPath)
if path.startswith('/home'):
return object.__new__(LinuxPath)
if path.startswith('/Users'):
return object.__new__(macOSPath)
instance.__init__(path)
return instance
def __init__(self, filename):
self.filename = filename
@abstractmethod
def dir_create(self): pass
@abstractmethod
def dir_list(self): pass
@abstractmethod
def dir_remove(self): pass
class WindowsPath(Path):
def dir_create(self):
print('create directory on ')
def dir_list(self):
print('list directory on ')
def dir_remove(self):
print('remove directory on ')
class LinuxPath(Path):
def dir_create(self):
print('create directory on ')
def dir_list(self):
print('list directory on ')
def dir_remove(self):
print('remove directory on ')
class macOSPath(Path):
def dir_create(self):
print('create directory on ')
def dir_list(self):
print('list directory on ')
def dir_remove(self):
print('remove directory on ')
if __name__ == '__main__':
file = Path(r'C:\Users\MWatney\myfile.txt')
print(type(file))
# <class '__main__.WindowsPath'>
file = Path(r'/home/mwatney/myfile.txt')
print(type(file))
# <class '__main__.LinuxPath'>
file = Path(r'/Users/mwatney/myfile.txt')
print(type(file))
# <class '__main__.macOSPath'>
7.3.12. Use Case - 9
from abc import ABC, abstractmethod
from typing import Any
TEMPLATE = """
<h1>Products</h1>
{% for product in products %}
<p>{{ product.title }}</p>
"""
class ViewEngine(ABC):
@abstractmethod
def render(self, view_name: str, context: dict[str, Any]): ...
class MatchaViewEngine(ViewEngine):
def render(self, view_name: str, context: dict[str, Any]) -> str:
return 'View rendered by Matcha'
class Controller:
def render(self, view_name: str, context: dict[str, Any], engine: ViewEngine) -> None:
html = engine.render(view_name, context)
print(html)
class ProductsController(Controller):
def list_products(self) -> None:
context: dict[str, Any] = {}
# get products from a database
# context[products] = products
self.render('products.html', context, MatchaViewEngine())
7.3.13. Use Case - 10
from abc import ABC, abstractmethod
from typing import Any
TEMPLATE = """
<h1>Products</h1>
{% for product in products %}
<p>{{ product.title }}</p>
"""
class ViewEngine(ABC):
@abstractmethod
def render(self, view_name: str, context: dict[str, Any]): ...
class MatchaViewEngine(ViewEngine):
def render(self, view_name: str, context: dict[str, Any]) -> str:
return 'View rendered by Matcha'
class Controller:
def _create_view_engine(self) -> ViewEngine:
return MatchaViewEngine()
def render(self, view_name: str, context: dict[str, Any]) -> None:
engine = self._create_view_engine()
html = engine.render(view_name, context)
print(html)
class SharpViewEngine(ViewEngine):
def render(self, view_name: str, context: dict[str, Any]):
return 'View rendered by Sharp'
class SharpController(Controller):
def _create_view_engine(self) -> ViewEngine:
return SharpViewEngine()
class ProductsController(SharpController):
def list_products(self) -> None:
context: dict[str, Any] = {}
# get products from a database
# context[products] = products
self.render('products.html', context)
if __name__ == '__main__':
ProductsController().list_products()
7.3.14. Assignments
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: DesignPatterns Creational FactoryMethod
# - Difficulty: easy
# - Lines: 6
# - Minutes: 8
# %% English
# 1. Create polymorphism factory `iris` producing instances of `Iris`
# 2. Separate `values` from `species` in each row
# 3. Create instances of:
# - class `Setosa` if `species` is "setosa"
# - class `Versicolor` if `species` is "versicolor"
# - class `Virginica` if `species` is "virginica"
# 4. Initialize instances with `values`
# 5. Run doctests - all must succeed
# %% Polish
# 1. Stwórz fabrykę abstrakcyjną `iris` tworzącą instancje klasy `Iris`
# 2. Odseparuj `values` od `species` w każdym wierszu
# 3. Stwórz instancje:
# - klasy `Setosa` jeżeli `species` to "setosa"
# - klasy `Versicolor` jeżeli `species` to "versicolor"
# - klasy `Virginica` jeżeli `species` to "virginica"
# 4. Instancje inicjalizuj danymi z `values`
# 5. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - `globals()[classname]`
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from pprint import pprint
>>> result = map(iris, DATA[1:])
>>> pprint(list(result), width=120)
[Virginica(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9),
Setosa(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2),
Versicolor(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3),
Virginica(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8),
Versicolor(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5),
Setosa(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2)]
"""
from dataclasses import dataclass
DATA = [
('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
(5.8, 2.7, 5.1, 1.9, 'virginica'),
(5.1, 3.5, 1.4, 0.2, 'setosa'),
(5.7, 2.8, 4.1, 1.3, 'versicolor'),
(6.3, 2.9, 5.6, 1.8, 'virginica'),
(6.4, 3.2, 4.5, 1.5, 'versicolor'),
(4.7, 3.2, 1.3, 0.2, 'setosa'),
]
@dataclass
class Iris:
sepal_length: float
sepal_width: float
petal_length: float
petal_width: float
class Setosa(Iris):
pass
class Versicolor(Iris):
pass
class Virginica(Iris):
pass
# Skip header and separate `values` from `species` in each row
# Create instances of: `Setosa`, `Versicolor`, `Virginica` based on `species`
# Initialize instances with `values`
# type: Callable[[tuple[float,float,float,float,str], Iris]]
def iris(row: tuple[float,float,float,float,str]) -> Iris:
...