4.5. Builder
Why: To separate the construction of an object from its representation
Why: The same construction algorithm can be applied to different representations
Usecase: Export data to different formats
Use the builder pattern to separate the exporting logic from the presentation format
The same exporting logic belongs to the different formats
The Builder design pattern is a creational design pattern that separates the construction of a complex object from its representation. It is useful when the construction process must allow different representations for the object that's constructed.
4.5.1. Problem
Builder Pattern is rarely used in Python, because there are keyword arguments
In other programming languages like Java, C#, C++, etc. the Builder Pattern is used to emulate keyword arguments
>>> from datetime import date
>>>
>>>
>>> class User:
... def __init__(self, username, password, firstname, lastname, birthdate, email, phone, is_active, is_staff, is_admin):
... self.username = username
... self.password = password
... self.firstname = firstname
... self.lastname = lastname
... self.birthdate = birthdate
... self.email = email
... self.phone = phone
... self.is_active = is_active
... self.is_staff = is_staff
... self.is_admin = is_admin
Positional Arguments:
>>> alice = User('alice', 'secret', 'Alice', 'Apricot', date(2000,1,1), 'alice@example.com', '+1 (234) 567-8910', True, True, False)
Keyword Arguments:
>>> alice = User(
... username='alice',
... password='secret',
... firstname='Alice',
... lastname='Apricot',
... birthdate=date(2000,1,1),
... email='alice@example.com',
... phone='+1 (234) 567-8910',
... is_active=True,
... is_staff=True,
... is_admin=False,
... )
4.5.2. Solution
>>> from datetime import date
>>>
>>>
>>> class User:
... def __init__(self, username):
... self.username = username
... self.password = None
... self.firstname = None
... self.lastname = None
... self.birthdate = None
... self.email = None
... self.phone = None
... self.is_active = False
... self.is_staff = False
... self.is_admin = False
...
... def with_password(self, password):
... self.password = password
... return self
...
... def with_firstname(self, firstname):
... self.firstname = firstname
... return self
...
... def with_lastname(self, lastname):
... self.lastname = lastname
... return self
...
... def with_birthdate(self, birthdate):
... self.birthdate = birthdate
... return self
...
... def with_email(self, email):
... self.email = email
... return self
...
... def with_phone(self, phone):
... self.phone = phone
... return self
...
... def with_is_active(self, is_active):
... self.is_active = is_active
... return self
...
... def with_is_staff(self, is_staff):
... self.is_staff = is_staff
... return self
...
... def with_is_admin(self, is_admin):
... self.is_admin = is_admin
... return self
Usage:
>>> alice = (
... User('alice')
... .with_password('secret')
... .with_firstname('Alice')
... .with_lastname('Apricot')
... .with_birthdate(date(2000, 1, 1))
... .with_email('alice@example.com')
... .with_phone('+1 (234) 567-8910')
... .with_is_active(True)
... .with_is_staff(True)
... .with_is_admin(False)
... )
4.5.3. Rationale
Using the Builder pattern allows for a complex validation for each attribute
Using methods to set each attribute allows for a more readable and maintainable code
Alternative is to put all that validation logic in the
__init__
method
>>> from datetime import date
>>> import re
>>> import hashlib
>>>
>>>
>>> class User:
... def __init__(self, username):
... self.username = username
... self.password = None
... self.firstname = None
... self.lastname = None
... self.birthdate = None
... self.email = None
... self.phone = None
... self.is_active = False
... self.is_staff = False
... self.is_admin = False
...
... def with_password(self, password):
... salt = self.username
... string = (salt+password).encode('utf-8')
... self.password = hashlib.sha512(string).hexdigest()
... return self
...
... def with_firstname(self, firstname):
... self.firstname = firstname.title()
... return self
...
... def with_lastname(self, lastname):
... self.lastname = lastname.title()
... return self
...
... def with_birthdate(self, birthdate):
... self.birthdate = birthdate
... td = (date.today() - birthdate)
... age = int(td.days // 365.25)
... if age < 18:
... raise ValueError('User must be at least 18 years old')
... return self
...
... def with_email(self, email, pattern='^[a-z]+@example.com$'):
... if not re.match(pattern, email):
... raise ValueError('Invalid email address')
... self.email = email
... return self
...
... def with_phone(self, phone, pattern=r'^\+\d \(\d{3}\) \d{3}-\d{4}$'):
... if not re.match(pattern, phone):
... raise ValueError('Invalid phone number')
... self.phone = phone
... return self
...
... def with_is_active(self, is_active):
... if type(is_active) is not bool:
... raise TypeError('is_active must be a boolean')
... self.is_active = is_active
... return self
...
... def with_is_staff(self, is_staff):
... if type(is_staff) is not bool:
... raise TypeError('is_staff must be a boolean')
... self.is_staff = is_staff
... return self
...
... def with_is_admin(self, is_admin):
... if type(is_admin) is not bool:
... raise TypeError('is_admin must be a boolean')
... self.is_admin = is_admin
... return self
Usage:
>>> alice = (
... User('alice')
... .with_password('secret')
... .with_firstname('Alice')
... .with_lastname('Apricot')
... .with_birthdate(date(2000, 1, 1))
... .with_email('alice@example.com')
... .with_phone('+1 (234) 567-8910')
... .with_is_active(True)
... .with_is_staff(True)
... .with_is_admin(False)
... )
>>>
>>> alice.password
'bb3847a34a2e503364b7ba4a49bd71d6fe10bf5494891ac137afedc17278094d0cdcbeed4f21c25e0b4c6a81e35c78a7a911a9ccbe5af670555f3dcc2b2d2030'
4.5.4. Case Study
Problem:
class CSV:
def __init__(self, filename, delimiter, encoding, quotechar, lineterminator, verbose):
self.filename = filename
self.delimiter = delimiter
self.encoding = encoding
self.quotechar = quotechar
self.lineterminator = lineterminator
self.verbose = verbose
if __name__ == '__main__':
file = CSV('/tmp/myfile.csv', ',', 'utf-8', '"', '\n', True)
Solution 1:
class CSV:
def __init__(self, filename):
self.filename = filename
def with_delimiter(self, delimiter):
self.delimiter = delimiter
return self
def with_encoding(self, encoding):
self.encoding = encoding
return self
def with_quotechar(self, quotechar):
self.quotechar = quotechar
return self
def with_lineterminator(self, lineterminator):
self.lineterminator = lineterminator
return self
def with_verbose(self, verbose):
self.verbose = verbose
return self
if __name__ == '__main__':
file = (
CSV('/tmp/myfile.csv')
.with_delimiter(',')
.with_encoding('utf-8')
.with_quotechar('"')
.with_lineterminator('\n')
.with_verbose(True)
)
Solution 2:
class CSV:
def __init__(self, filename, delimiter, encoding, quotechar,
lineterminator, verbose):
self.filename = filename
self.delimiter = delimiter
self.encoding = encoding
self.quotechar = quotechar
self.lineterminator = lineterminator
self.verbose = verbose
if __name__ == '__main__':
file = CSV(
filename='/tmp/myfile.csv',
delimiter=',',
encoding='utf-8',
quotechar='"',
lineterminator='\n',
verbose=True,
)
Diagram:

4.5.5. Use Case - 1
def clean(text):
return (text
# Convert to common format
.lower()
.strip()
# Remove unwanted whitespaces
.replace('\n', ' ')
.replace('\t', ' ')
.replace(' ', ' ')
.replace(' ', ' ')
.replace(' ', ' ')
.replace(' ', ' ')
.replace(' ', ' ')
.strip()
# Remove unwanted special characters
.replace('!', '')
.replace('@', '')
.replace('#', '')
.replace('$', '')
.replace('%', '')
.replace('^', '')
.replace('&', '')
.replace('*', '')
.replace('(', '')
.replace(')', '')
.replace('+', '')
.replace('=', '')
.replace('_', '')
.replace('\\', '')
.replace("'", '')
.replace('"', '')
.strip()
# Remove unwanted fragments
.removeprefix('ulica')
.removeprefix('osiedle')
.removeprefix('plac')
.removeprefix('aleja')
.removeprefix('ul.')
.removeprefix('os.')
.removeprefix('pl.')
.removeprefix('al.')
.removeprefix('ul ')
.removeprefix('os ')
.removeprefix('pl ')
.removeprefix('al ')
.strip()
# Replace numbers
.replace('trzeciego', 'III')
.replace('drugiego', 'II')
.replace('pierwszego', 'I')
.replace('3', 'III')
.replace('2', 'II')
.replace('1', 'I')
.strip()
# Formatting output
.title()
.replace('Iii', 'III')
.replace('Ii', 'II')
.strip()
)
4.5.6. Use Case - 2
import pandas as pd
# PKB = 'https://pl.wikipedia.org/wiki/Lista_pa%C5%84stw_%C5%9Bwiata_wed%C5%82ug_PKB_nominalnego'
PKB = 'https://python3.info/_static/percapita-pkb.html'
USD = 1
# %% Problem
pkb = pd.read_html(PKB)[1]
pkb = pkb.rename(columns={'Państwo':'kraj', '2021 r.':'pkb'})
pkb = pkb.loc[:, ['kraj', 'pkb']]
pkb = pkb.replace({'pkb': {'\xa0': '', 'b.d.': pd.NA}}, regex=True)
pkb = pkb.dropna(how='any', axis='rows')
pkb = pkb.astype({'kraj': 'str', 'pkb': 'int64'})
pkb = pkb.convert_dtypes()
pkb = pkb.set_index('kraj', drop=True)
pkb = pkb.mul(1_000_000*USD)
# %% Solution
pkb = (
pd
.read_html(PKB)[1]
.rename(columns={'Państwo':'kraj', '2021 r.':'pkb'})
.loc[:, ['kraj', 'pkb']]
.replace({'pkb': {'\xa0': '', 'b.d.': pd.NA}}, regex=True)
.dropna(how='any', axis='rows')
.astype({'kraj': 'str', 'pkb': 'int64'})
.convert_dtypes()
.set_index('kraj', drop=True)
.mul(1_000_000*USD)
)
4.5.7. Use Case - 3
from enum import Enum
class Slide:
text: str
def __init__(self, text: str) -> None:
self.text = text
def get_text(self) -> str:
return self.text
## Formats
class PresentationFormat(Enum):
PDF = 1
IMAGE = 2
POWERPOINT = 3
MOVIE = 4
class PDFDocument:
def add_page(self, text: str) -> None:
print('Adding a page to PDF')
class Movie:
def add_frame(self, text: str, duration: int) -> None:
print('Adding a frame to a movie')
## Main
class Presentation:
slides: list[Slide]
def __init__(self) -> None:
self.slides = []
def add_slide(self, slide: Slide) -> None:
self.slides.append(slide)
def export(self, format: PresentationFormat) -> None:
if format == PresentationFormat.PDF:
pdf = PDFDocument()
pdf.add_page('Copyright')
for slide in self.slides:
pdf.add_page(slide.get_text())
elif format == PresentationFormat.MOVIE:
movie = Movie()
movie.add_frame('Copyright', duration=3)
for slide in self.slides:
movie.add_frame(slide.get_text(), duration=3)
4.5.8. Use Case - 4
from enum import Enum
class Slide:
text: str
def __init__(self, text: str) -> None:
self.text = text
def get_text(self) -> str:
return self.text
class PresentationBuilder:
def add_slide(self, slide: Slide) -> None:
raise NotImplementedError
## Formats
class PresentationFormat(Enum):
PDF = 1
IMAGE = 2
POWERPOINT = 3
MOVIE = 4
class PDFDocument:
def add_page(self, text: str) -> None:
print('Adding a page to PDF')
class Movie:
def add_frame(self, text: str, duration: int) -> None:
print('Adding a frame to a movie')
class PDFDocumentBuilder(PresentationBuilder):
document: PDFDocument
def __init__(self):
self.document = PDFDocument()
def add_slide(self, slide: Slide) -> None:
self.document.add_page(slide.get_text())
def get_pdf_document(self) -> PDFDocument:
return self.document
class MovieBuilder(PresentationBuilder):
movie: Movie
def __init__(self):
self.movie = Movie()
def add_slide(self, slide: Slide) -> None:
self.movie.add_frame(slide.get_text(), duration=3)
def get_movie(self) -> Movie:
return self.movie
## Main
class Presentation:
slides: list[Slide]
def __init__(self) -> None:
self.slides = []
def add_slide(self, slide: Slide) -> None:
self.slides.append(slide)
def export(self, builder: PresentationBuilder) -> None:
builder.add_slide(Slide('Copyright'))
for slide in self.slides:
builder.add_slide(slide)
if __name__ == '__main__':
presentation = Presentation()
presentation.add_slide(Slide('Slide 1'))
presentation.add_slide(Slide('Slide 2'))
builder = PDFDocumentBuilder()
presentation.export(builder)
movie = builder.get_pdf_document()
builder = MovieBuilder()
presentation.export(builder)
movie = builder.get_movie()
4.5.9. Use Case - 5
When language does not have keyword arguments to functions and methods
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html
>>> def read_csv(filepath_or_buffer, sep=', ', delimiter=None, header='infer',
... names=None, index_col=None, usecols=None, squeeze=False,
... prefix=None, mangle_dupe_cols=True, dtype=None, engine=None,
... converters=None, true_values=None, false_values=None,
... skipinitialspace=False, skiprows=None, nrows=None,
... na_values=None, keep_default_na=True, na_filter=True,
... verbose=False, skip_blank_lines=True, parse_dates=False,
... infer_datetime_format=False, keep_date_col=False,
... date_parser=None, dayfirst=False, iterator=False,
... chunksize=None, compression='infer', thousands=None,
... decimal=b'.', lineterminator=None, quotechar='"',
... quoting=0, escapechar=None, comment=None, encoding=None,
... dialect=None, tupleize_cols=None, error_bad_lines=True,
... warn_bad_lines=True, skipfooter=0, doublequote=True,
... delim_whitespace=False, low_memory=True, memory_map=False,
... float_precision=None): ...
Positional arguments:
>>> data = read_csv('/tmp/myfile.csv', ', ', None, 'infer', None, None, None,
... False, None, True, None, None, None, None, None, False,
... None, None, None, True, True, False, True, False, False,
... False, None, False, False, None, 'infer', None, b'.',
... None, '"', 0, None, None, None, None, None, True, True,
... 0, True, False, True, False, None)
Keyword arguments:
>>> data = read_csv('/tmp/myfile.csv',
... chunksize=10_000,
... delimiter=',',
... encoding='utf-8')
4.5.10. Use Case - 6
>>> class Person:
... def __init__(self, firstname, lastname, email, age, height, weight):
... self.firstname = firstname
... self.lastname = lastname
... self.email = email
... self.age = age
... self.height = height
... self.weight = weight
>>> alice = Person('Alice', 'Apricot', 'alice@example.com', 30, 170.0, 55.5)
>>> alice = Person(
... firstname='Alice',
... lastname='Apricot',
... email='alice@example.com',
... age=30,
... height=170.0,
... weight=55.5,
... )
4.5.11. Use Case - 7
>>> class Person:
... def __init__(self, firstname, lastname,
... is_active, is_staff, is_admin,
... groups, following, followers):
... ...
>>> alice = Person('Alice', 'Apricot', True, True, True, None, 1, 17)
>>> alice = Person(
... firstname = 'Alice',
... lastname = 'Apricot',
... is_active = True,
... is_staff = True,
... is_admin = True,
... groups = None,
... following = 1,
... followers = 17,
... )
>>> class Person:
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
... self.is_active = None
... self.is_staff = None
... self.is_admin = None
... self.groups = None
... self.following = None
... self.followers = None
...
... def with_is_active(self, is_active):
... self.is_active = is_active
... return self
...
... def with_is_staff(self, is_staff):
... self.is_staff = is_staff
... return self
...
... def with_is_admin(self, is_admin):
... self.is_admin = is_admin
... return self
...
... def with_groups(self, groups):
... self.groups = groups
... return self
...
... def with_following(self, following):
... self.following = following
... return self
...
... def with_followers(self, followers):
... self.followers = followers
... return self
>>>
>>>
>>> alice = (
... Person('Alice', 'Apricot')
... .with_is_active(True)
... .with_is_staff(True)
... .with_is_admin(True)
... .with_groups(None)
... .with_following(1)
... .with_followers(17)
... )
4.5.12. Assignments
# %% About
# - Name: DesignPatterns Creational BuilderEmail
# - Difficulty: easy
# - Lines: 15
# - Minutes: 5
# %% 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 class `Email`
# 2. Use builder pattern to set:
# - `recipient: str`
# - `sender: str`
# - `subject: str`
# - `body: str`
# 3. Remember to initialize all values in `__init__`
# 4. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Email`
# 2. Użyj wzorca builder, aby ustawić:
# - `recipient: str`
# - `sender: str`
# - `subject: str`
# - `body: str`
# 3. Pamiętaj o metodzie `__init__` inicjującej wszystkie wartości
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from pprint import pprint
>>> result = Email()
>>> vars(result)
{'recipient': None, 'sender': None, 'subject': None, 'body': None}
>>> result = (
... Email()
... .with_recipient('mwatney@nasa.gov')
... .with_sender('mlewis@nasa.gov')
... .with_subject('Hello from Mars')
... .with_body('Greetings from Red Planet')
... )
>>> pprint(vars(result), width=72, sort_dicts=False)
{'recipient': 'mwatney@nasa.gov',
'sender': 'mlewis@nasa.gov',
'subject': 'Hello from Mars',
'body': 'Greetings from Red Planet'}
"""
# %% 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
# %% Types
from typing import Callable
Email: type
with_recipient: Callable[[object, str], object]
with_sender: Callable[[object, str], object]
with_subject: Callable[[object, str], object]
with_body: Callable[[object, str], object]
# %% Data
# %% Result
class Email:
recipient: str
sender: str
subject: str
body: str
attachment: bytes
# %% About
# - Name: DesignPatterns Creational BuilderEmail
# - Difficulty: easy
# - Lines: 2
# - Minutes: 2
# %% 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 class `Email`
# 2. Use builder pattern to set:
# - `subject: str` encode to bytes (utf-8 encoding)
# - `body: str` encode to bytes (utf-8 encoding)
# 3. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Email`
# 2. Użyj wzorca builder, aby ustawić:
# - `subject: str` enkoduje do bajtów (kodowanie utf-8)
# - `body: str` enkoduje na bajtów (kodowanie utf-8)
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - `str.encode('utf-8')`
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> result = Email()
>>> vars(result)
{'recipient': None, 'sender': None, 'subject': None, 'body': None}
>>> result = Email()
>>> assert result.with_subject('cześć').subject == b'cze\\xc5\\x9b\\xc4\\x87', \
'Encode subject with utf-8'
>>> result = Email()
>>> assert result.with_body('cześć').body == b'cze\\xc5\\x9b\\xc4\\x87', \
'Encode body with utf-8'
>>> result = (
... Email()
... .with_recipient('mwatney@nasa.gov')
... .with_sender('mlewis@nasa.gov')
... .with_subject('Hello from Mars')
... .with_body('Greetings from Red Planet')
... )
>>> from pprint import pprint
>>> pprint(vars(result), width=72, sort_dicts=False)
{'recipient': 'mwatney@nasa.gov',
'sender': 'mlewis@nasa.gov',
'subject': b'Hello from Mars',
'body': b'Greetings from Red Planet'}
"""
# %% 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
# %% Types
from typing import Callable
Email: type
with_recipient: Callable[[object, str], object]
with_sender: Callable[[object, str], object]
with_subject: Callable[[object, str], object]
with_body: Callable[[object, str], object]
# %% Data
# %% Result
class Email:
recipient: str
sender: str
subject: bytes
body: bytes
def __init__(self):
self.recipient = None
self.sender = None
self.subject = None
self.body = None
def with_recipient(self, recipient):
self.recipient = recipient
return self
def with_sender(self, sender):
self.sender = sender
return self
def with_subject(self, subject):
self.subject = subject
return self
def with_body(self, body):
self.body = body
return self
# %% About
# - Name: DesignPatterns Creational BuilderEmail
# - Difficulty: easy
# - Lines: 4
# - Minutes: 3
# %% 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 class `Email`
# 2. Use builder pattern to set:
# - `recipient: str` verify email address using regex
# - `sender: str` verify email address using regex
# 3. For email validation use regex pattern: `r'^[a-z]+@example.com$'`
# 4. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Email`
# 2. Użyj wzorca builder, aby ustawić:
# - `recipient: str` zweryfikuj adres e-mail za pomocą wyrażenia regularnego
# - `sender: str` zweryfikuj adres e-mail za pomocą wyrażenia regularnego
# 3. Do walidacji email użyj wzorca regex: `r'^[a-z]+@example.com$'`
# 4. Jeżeli adres e-mail nie jest poprawny, zgłoś wyjątek `ValueError`
# z komunikatem: `Invalid recipient` lub `Invalid sender`
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% Hints
# - `re.match()`
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> result = Email()
>>> vars(result)
{'recipient': None, 'sender': None, 'subject': None, 'body': None}
>>> result = Email()
>>> result.with_recipient('mallory@example.net')
Traceback (most recent call last):
ValueError: Invalid recipient
>>> result = Email()
>>> result.with_sender('mallory@example.net')
Traceback (most recent call last):
ValueError: Invalid sender
>>> result = (
... Email()
... .with_recipient('alice@example.com')
... .with_sender('bob@example.com')
... .with_subject('Hello from Mars')
... .with_body('Greetings from Red Planet')
... )
>>> from pprint import pprint
>>> pprint(vars(result), width=72, sort_dicts=False)
{'recipient': 'alice@example.com',
'sender': 'bob@example.com',
'subject': b'Hello from Mars',
'body': b'Greetings from Red Planet'}
"""
# %% 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
import re
# %% Types
from typing import Callable
Email: type
with_recipient: Callable[[object, str], object]
with_sender: Callable[[object, str], object]
with_subject: Callable[[object, str], object]
with_body: Callable[[object, str], object]
# %% Data
# %% Result
class Email:
recipient: str
sender: str
subject: bytes
body: bytes
def __init__(self):
self.recipient = None
self.sender = None
self.subject = None
self.body = None
def with_recipient(self, recipient):
self.recipient = recipient
return self
def with_sender(self, sender):
self.sender = sender
return self
def with_subject(self, subject):
self.subject = subject.encode('utf-8')
return self
def with_body(self, body):
self.body = body.encode('utf-8')
return self
# %% About
# - Name: DesignPatterns Creational BuilderEmail
# - Difficulty: easy
# - Lines: 2
# - Minutes: 3
# %% 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 class `Email`
# 2. Use builder pattern to set:
# - `attachment: bytes` base64 encoded
# 3. Remember to initialize all values in `__init__`
# 4. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Email`
# 2. Użyj wzorca builder, aby ustawić:
# - `attachment: bytes` zakodowane w standardzie base64
# 3. Pamiętaj o metodzie `__init__` inicjującej wszystkie wartości
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from pprint import pprint
>>> result = Email()
>>> vars(result)
{'recipient': None, 'sender': None, 'subject': None, 'body': None, 'attachment': None}
>>> result = (
... Email()
... .with_recipient('mwatney@example.com')
... .with_sender('mlewis@example.com')
... .with_subject('Hello from Mars')
... .with_body('Greetings from Red Planet')
... .with_attachment(b'myfile.txt')
... )
>>> pprint(vars(result), width=72, sort_dicts=False)
{'recipient': 'mwatney@example.com',
'sender': 'mlewis@example.com',
'subject': b'Hello from Mars',
'body': b'Greetings from Red Planet',
'attachment': b'bXlmaWxlLnR4dA=='}
"""
# %% 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
import re
from base64 import b64encode
# %% Types
from typing import Callable
Email: type
with_recipient: Callable[[object, str], object]
with_sender: Callable[[object, str], object]
with_subject: Callable[[object, str], object]
with_body: Callable[[object, str], object]
with_attachment: Callable[[object, bytes], object]
# %% Data
# %% Result
class Email:
recipient: str
sender: str
subject: bytes
body: bytes
def __init__(self):
self.recipient = None
self.sender = None
self.subject = None
self.body = None
def with_recipient(self, recipient, pattern=r'^[a-z]+@example.com$'):
if not re.match(pattern, recipient):
raise ValueError('Invalid recipient')
self.recipient = recipient
return self
def with_sender(self, sender, pattern=r'^[a-z]+@example.com$'):
if not re.match(pattern, sender):
raise ValueError('Invalid sender')
self.sender = sender
return self
def with_subject(self, subject):
self.subject = subject.encode('utf-8')
return self
def with_body(self, body):
self.body = body.encode('utf-8')
return self
def with_attachment(self, attachment):
...
# %% About
# - Name: DesignPatterns Creational BuilderTexture
# - Difficulty: easy
# - Lines: 18
# - Minutes: 5
# %% 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 class `Texture`
# 2. Use builder pattern to set:
# - `file: str`
# - `width: int` value greater than 0
# - `height: int` value greater than 0
# - `quality: int` from 1 to 100 percent
# 3. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Texture`
# 2. Użyj wzorca builder, aby ustawić:
# - `file: str`
# - `width: int` wartość większa niż 0
# - `height: int` wartość większa niż 0
# - `quality: int` od 1 do 100 procent
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from pprint import pprint
>>> result = (
... Texture()
... .with_file('img/dragon/alive.png')
... .with_height(100)
... .with_width(50)
... .with_quality(75)
... )
>>> vars(result)
{'file': 'img/dragon/alive.png', 'height': 100, 'width': 50, 'quality': 75}
"""
# %% 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
# %% Types
from typing import Callable
# %% Data
Texture: type
with_file: Callable[[object, str], object]
with_width: Callable[[object, int], object]
with_height: Callable[[object, int], object]
with_quality: Callable[[object, int], object]
# %% Result
class Texture:
file: str
width: int
height: int
quality: int