4.6. Prototype
Create new object by copying an existing object
The Prototype design pattern is a creational design pattern that allows cloning objects, even complex ones, without coupling to their specific classes. All prototype classes have a common interface that makes it possible to clone objects.
4.6.1. Problem
>>> class User:
... def __init__(self, username, password, is_user, is_staff, is_admin):
... self.username = username
... self.password = password
... self.is_user = is_user
... self.is_staff = is_staff
... self.is_admin = is_admin
>>>
>>>
>>> alice = User(
... username='alice',
... password='secret',
... is_user=True,
... is_staff=True,
... is_admin=False,
... )
>>>
>>> bob = User(
... username=alice.username,
... password=alice.password,
... is_user=alice.is_user,
... is_staff=alice.is_staff,
... is_admin=alice.is_admin,
... )
4.6.2. Solution
>>> class User:
... def __init__(self, username, password, is_user, is_staff, is_admin):
... self.username = username
... self.password = password
... self.is_user = is_user
... self.is_staff = is_staff
... self.is_admin = is_admin
...
... def clone(self):
... data = vars(self)
... return User(**data)
>>>
>>>
>>> alice = User(
... username='alice',
... password='secret',
... is_user=True,
... is_staff=True,
... is_admin=False,
... )
>>>
>>> bob = alice.clone()
The clone method creates a new instance of the User class, and those two instances are different objects:
>>> alice
<__main__.User object at 0x105f69160>
>>>
>>> bob
<__main__.User object at 0x105f3efd0>
But, they have the same data:
>>> vars(alice)
{'username': 'alice', 'password': 'secret', 'is_user': True, 'is_staff': True, 'is_admin': False}
>>>
>>> vars(bob)
{'username': 'alice', 'password': 'secret', 'is_user': True, 'is_staff': True, 'is_admin': False}
4.6.3. Rationale
You can customize object on cloning
>>> class User:
... def __init__(self, username, password, is_user, is_staff, is_admin):
... self.username = username
... self.password = password
... self.is_user = is_user
... self.is_staff = is_staff
... self.is_admin = is_admin
...
... def clone(self, **kwargs):
... data = vars(self) | kwargs
... return User(**data)
>>>
>>>
>>> alice = User(
... username='alice',
... password='secret',
... is_user=True,
... is_staff=True,
... is_admin=False,
... )
>>>
>>> bob = alice.clone(username='bob', password='qwerty')
The clone method creates a new instance of the User class, and those two instances are different objects:
>>> alice
<__main__.User object at 0x105f69160>
>>>
>>> bob
<__main__.User object at 0x105f3efd0>
But, because we passed new values to the clone method, they have different data - some fields are overwritten:
>>> vars(alice)
{'username': 'alice', 'password': 'secret', 'is_user': True, 'is_staff': True, 'is_admin': False}
>>>
>>> vars(bob)
{'username': 'bob', 'password': 'qwerty', 'is_user': True, 'is_staff': True, 'is_admin': False}
4.6.4. Case Study
Problem:
class User:
def __init__(self, firstname, lastname, email):
self.firstname = firstname
self.lastname = lastname
self.email = email
if __name__ == '__main__':
a = User('Mark', 'Watney', 'mwatney@nasa.gov')
b = User( a.firstname, a.lastname, a.email)
Solution:
class User:
def __init__(self, firstname, lastname, email):
self.firstname = firstname
self.lastname = lastname
self.email = email
def copy(self):
return User(
self.firstname,
self.lastname,
self.email
)
if __name__ == '__main__':
a = User('Mark', 'Watney', email='mwatney@nasa.gov')
b = a.copy()
Diagram:

4.6.5. Use Case - 1
>>> data = [1, 2, 3]
>>> a = data
>>> b = data.copy()
>>>
>>>
>>> data
[1, 2, 3]
>>>
>>> a
[1, 2, 3]
>>>
>>> b
[1, 2, 3]
>>> data.append(4)
>>>
>>>
>>> data
[1, 2, 3, 4]
>>>
>>> a
[1, 2, 3, 4]
>>>
>>> b
[1, 2, 3]
4.6.6. Use Case - 2
from typing import Literal
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
firstname: str
lastname: str
username: str
password: str
email: str
last_login: datetime | None
role: Literal['admin', 'user', 'guest']
groups: list[str] = field(default_factory=list)
def clone(self):
return User(
firstname = self.firstname,
lastname = self.lastname,
username = self.username,
password = self.password,
email = self.email,
last_login = self.last_login,
role = self.role,
groups = self.groups)
mark = User(
firstname='Mark',
lastname='Watney',
username='mwatney',
password='Ares3',
email='mwatney@nasa.gov',
last_login=None,
role='admin',
groups=['admins', 'users'],
)
melissa = mark.clone()
print(melissa)
# User(firstname='Mark', lastname='Watney', username='mwatney', password='Ares3', email='mwatney@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
melissa.firstname = 'Melissa'
melissa.lastname = 'Lewis'
melissa.username = 'mlewis'
melissa.email = 'mlewis@nasa.gov'
print(melissa)
# User(firstname='Melissa', lastname='Lewis', username='mlewis', password='Ares3', email='mlewis@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
4.6.7. Use Case - 3
from typing import Literal
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
firstname: str
lastname: str
username: str
password: str
email: str
last_login: datetime | None
role: Literal['admin', 'user', 'guest']
groups: list[str] = field(default_factory=list)
def clone(self):
return User(**vars(self))
mark = User(
firstname='Mark',
lastname='Watney',
username='mwatney',
password='Ares3',
email='mwatney@nasa.gov',
last_login=None,
role='admin',
groups=['admins', 'users'],
)
melissa = mark.clone()
print(melissa)
# User(firstname='Mark', lastname='Watney', username='mwatney', password='Ares3', email='mwatney@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
melissa.firstname = 'Melissa'
melissa.lastname = 'Lewis'
melissa.username = 'mlewis'
melissa.email = 'mlewis@nasa.gov'
print(melissa)
# User(firstname='Melissa', lastname='Lewis', username='mlewis', password='Ares3', email='mlewis@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
4.6.8. Use Case - 4
from typing import Literal
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
firstname: str
lastname: str
username: str
password: str
email: str
last_login: datetime | None
role: Literal['admin', 'user', 'guest']
groups: list[str] = field(default_factory=list)
def clone(self, **kwargs):
values = vars(self) | kwargs
return User(**values)
mark = User(
firstname='Mark',
lastname='Watney',
username='mwatney',
password='Ares3',
email='mwatney@nasa.gov',
last_login=None,
role='admin',
groups=['admins', 'users'],
)
melissa = mark.clone(
firstname='Melissa',
lastname='Lewis',
username='mlewis',
email='mlewis@nasa.gov',
)
print(melissa)
# User(firstname='Melissa', lastname='Lewis', username='mwatney', password='Ares3', email='mwatney@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
4.6.9. Use Case - 5
from typing import Literal
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
firstname: str
lastname: str
username: str
password: str
email: str
last_login: datetime | None
role: Literal['admin', 'user', 'guest']
groups: list[str] = field(default_factory=list)
def clone(self, **kwargs):
values = vars(self) | kwargs
cls = self.__class__
return cls(**values)
@dataclass
class Admin(User):
pass
mark = Admin(
firstname='Mark',
lastname='Watney',
username='mwatney',
password='Ares3',
email='mwatney@nasa.gov',
last_login=None,
role='admin',
groups=['admins', 'users'],
)
melissa = mark.clone(
firstname='Melissa',
lastname='Lewis',
username='mlewis',
email='mlewis@nasa.gov',
)
print(melissa)
# Admin(firstname='Melissa', lastname='Lewis', username='mwatney', password='Ares3', email='mwatney@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
4.6.10. Use Case - 6
>>> import copy
>>> from dataclasses import dataclass, field
>>>
>>>
>>> @dataclass
... class Account:
... username: str
...
... def clone(self):
... return copy.deepcopy(self)
>>>
>>>
>>> @dataclass
... class User(Account):
... permissions: list = field(default_factory=lambda: ["read", "comment"])
... is_active: bool = True
...
... def __post_init__(self):
... self.account_type = "user"
>>>
>>>
>>> @dataclass
... class Admin(Account):
... permissions: list = field(default_factory=lambda: ["read", "write", "delete", "admin"])
... is_active: bool = True
...
... def __post_init__(self):
... self.account_type = "admin"
>>>
>>>
>>> # Main
>>> def main():
... # Create prototype instances
... alice = User(username="alice")
... bob = Admin(username="bob")
...
... print(f"Original alice: {alice}")
... print(f"Original bob: {bob}")
...
... # Create clones
... alice_clone = alice.clone()
... bob_clone = bob.clone()
...
... # Verify clones are separate objects but with same data
... print(f"\nCloned alice: {alice_clone}")
... print(f"Cloned bob: {bob_clone}")
...
... # Modify clones
... alice_clone.permissions.append("upload")
... bob_clone.username = "bob_admin"
...
... # Show that originals are unchanged
... print(f"\nAfter modifications:")
... print(f"Original alice: {alice}")
... print(f"Original bob: {bob}")
... print(f"Modified alice clone: {alice_clone}")
... print(f"Modified bob clone: {bob_clone}")
4.6.11. Assignments
# %% About
# - Name: DesignPatterns Creational PrototypeDate
# - Difficulty: easy
# - Lines: 5
# - 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 `Date` with:
# - `year: int`
# - `month: int`
# - `day: int`
# - method `.clone()`
# 2. Method `.clone()` returns another `Date` with the same values
# 3. Do not use `vars(self)`
# 4. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Date` z:
# - `year: int`
# - `month: int`
# - `day: int`
# - metodą `.clone()`
# 2. Metoda `.clone()` zwraca kolejny `Date` z tymi samymi wartościami
# 3. Nie używaj `vars(self)`
# 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
>>> date = Date(1969, 7, 21)
>>> result = date.clone()
>>> result.year
1969
>>> result.month
7
>>> result.day
21
"""
# %% 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
Date: type
clone: Callable[[object], object]
# %% Data
# %% Result
@dataclass
class Date:
year: int
month: int
day: int
# %% About
# - Name: DesignPatterns Creational PrototypeTime
# - 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 `Time` with:
# - `hour: int`
# - `minute: int`
# - `second: int`
# - `microsecond: int`
# - method `.clone()`
# 2. Method `.clone()` returns another `Time` with the same values
# 3. Use `vars(self)`
# 4. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Time` z:
# - `hour: int`
# - `minute: int`
# - `second: int`
# - `microsecond: int`
# - metodą `.clone()`
# 2. Metoda `.clone()` zwraca kolejny `Time` z tymi samymi wartościami
# 3. Użyj `vars(self)`
# 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
>>> time = Time(2, 56, 15)
>>> result = time.clone()
>>> result.hour
2
>>> result.minute
56
>>> result.second
15
>>> result.microsecond
0
"""
# %% 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
Date: type
clone: Callable[[object], object]
# %% Data
# %% Result
@dataclass
class Time:
hour: int = 0
minute: int = 0
second: int = 0
microsecond: int = 0
# %% About
# - Name: DesignPatterns Creational PrototypeDragon
# - 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 class `Dragon`
# 2. Dragon has attributes:
# - `name: str`
# - `position: tuple[int,int]` default `(0, 0)`
# - `health: int` random from 50 to 100
# - `gold: int` random from 1 to 100
# - method `.clone()`
# 3. Method `.clone()` returns another `Dragon` with the same values
# 4. Use `random.randint()` to generate pseudorandom numbers
# 5. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Dragon`
# 2. Dragon ma atrybuty:
# - `name: str`
# - `position: tuple[int,int]` domyślnie `(0, 0)`
# - `health: int` losowe od 50 do 100
# - `gold: int` losowe od 1 do 100
# - metodę `.clone()`
# 3. Metoda `.clone()` zwraca kolejnego `Dragon` z tymi samymi wartościami
# 4. Użyj `random.randint()` do generowania pseudolosowych liczb
# 5. 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
>>> from random import seed
>>> seed(0)
>>> dragon = Dragon('Wawelski')
>>> result = dragon.clone()
>>> result.name
'Wawelski'
>>> result.health
74
>>> result.gold
98
>>> result.position
(0, 0)
"""
# %% 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, field
from random import randint, seed
# %% Types
from typing import Callable
Dragon: type
clone: Callable[[object], object]
# %% Data
seed(0)
# %% Result
@dataclass
class Dragon:
...