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:

../../_images/designpatterns-builder-casestudy.png

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

>>> 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