4.3. Factory Method
Defer the creation of an object to subclasses
Relays on inheritance and polymorphism
Adds flexibility to the design
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.
4.3.1. Problem
>>> class User:
... def __init__(self, uid, username):
... self.uid = uid
... self.username = username
...
... def login(self):
... print('User logged-in')
>>>
>>>
>>> class Admin:
... def __init__(self, uid, username):
... self.uid = uid
... self.username = username
...
... def login(self):
... print('Admin logged-in')
>>>
>>>
>>>
>>> alice = User(uid=1000, username='alice')
>>> bob = Admin(uid=999, username='bob')
>>>
>>> alice.login()
User logged-in
>>>
>>> bob.login()
Admin logged-in
4.3.2. Solution: Function
>>> class User:
... def __init__(self, uid, username):
... self.uid = uid
... self.username = username
...
... def login(self):
... print('User logged-in')
>>>
>>>
>>> class Admin:
... def __init__(self, uid, username):
... self.uid = uid
... self.username = username
...
... def login(self):
... print('Admin logged-in')
>>>
>>>
>>> def account(uid, username):
... if uid < 1000:
... return Admin(uid, username)
... else:
... return User(uid, username)
>>>
>>>
>>> alice = account(uid=1000, username='alice')
>>> bob = account(uid=999, username='bob')
>>>
>>> alice.login()
User logged-in
>>>
>>> bob.login()
Admin logged-in
4.3.3. Solution: Method
>>> class User:
... def __init__(self, uid, username):
... self.uid = uid
... self.username = username
...
... def login(self):
... print('User logged-in')
>>>
>>>
>>> class Admin:
... def __init__(self, uid, username):
... self.uid = uid
... self.username = username
...
... def login(self):
... print('Admin logged-in')
>>>
>>>
>>> class Account:
... def __new__(cls, uid, username):
... if uid < 1000:
... return super().__new__(Admin)
... else:
... return super().__new__(User)
>>>
>>>
>>> alice = Account(uid=1000, username='alice')
>>> root = Account(uid=999, username='bob')
>>>
>>> alice.login()
User logged-in
>>>
>>> bob.login()
Admin logged-in
4.3.4. Case Study
Tightly coupled with MatchaEngine
What if we have better templating engine
Problem:
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'>
Solution 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__':
file = document('myfile.pdf')
type(file)
# <class '__main__.PDF'>
Solution 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__':
file = Document('myfile.pdf')
type(file)
# <class '__main__.PDF'>
Diagram:

4.3.5. 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'>
4.3.6. 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'>
4.3.7. 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'>
4.3.8. 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
4.3.9. 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
4.3.10. 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()
4.3.11. 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
4.3.12. 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'>
4.3.13. Use Case - 9
from typing import Any
from abc import ABC, abstractmethod
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())
4.3.14. Use Case - 10
from typing import Any
from abc import ABC, abstractmethod
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()
4.3.15. Use Case - 11
>>> from abc import ABC, abstractmethod
>>> from dataclasses import dataclass
>>> from typing import List
>>>
>>>
>>> @dataclass
... class Permission:
... name: str
... level: int
>>>
>>>
>>> @dataclass
... class Account(ABC):
... username: str
...
... @abstractmethod
... def create_permissions(self):
... raise NotImplementedError
...
... def login(self):
... permissions = self.create_permissions()
... print(f"{self.username} logged in with permissions: {[p.name for p in permissions]}")
... return permissions
>>>
>>>
>>> @dataclass
... class User(Account):
... username: str
...
... def create_permissions(self):
... return [
... Permission(name="read", level=1),
... Permission(name="comment", level=2)
... ]
>>>
>>>
>>> @dataclass
... class Admin(Account):
... username: str
...
... def create_permissions(self):
... return [
... Permission(name="read", level=1),
... Permission(name="write", level=3),
... Permission(name="delete", level=5),
... Permission(name="admin_panel", level=10)
... ]
>>>
>>>
>>> # Main
>>> def main():
... # Create instances
... alice = User(username="alice")
... bob = Admin(username="bob")
...
... # Use the factory method
... alice_permissions = alice.login()
... bob_permissions = bob.login()
...
... # Check permissions
... is_alice_admin = any(p.name == "admin_panel" for p in alice_permissions)
... is_bob_admin = any(p.name == "admin_panel" for p in bob_permissions)
...
... print(f"Is alice an admin? {is_alice_admin}") # False
... print(f"Is bob an admin? {is_bob_admin}") # True
>>> main()
alice logged in with permissions: ['read', 'comment']
bob logged in with permissions: ['read', 'write', 'delete', 'admin_panel']
Is alice an admin? False
Is bob an admin? True
4.3.16. Assignments
# %% About
# - Name: DesignPatterns Creational FactoryMethod
# - Difficulty: easy
# - Lines: 6
# - Minutes: 8
# %% 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
# %% 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 inicjalizuje danymi z `values`
# 5. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - `globals()[classname]`
# %% Doctests
"""
>>> 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)]
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% Imports
from dataclasses import dataclass
# %% Types
from typing import Callable
iris: Callable[[tuple[float,float,float,float,str]], object]
# %% Data
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
# %% Result
def iris(row: tuple[float,float,float,float,str]) -> Iris:
...