Skip to content

Commit cfba954

Browse files
authored
Merge pull request #684 from realpython/python-mixins
Materials for Python Mixins
2 parents 1625a07 + 9e16069 commit cfba954

File tree

7 files changed

+337
-0
lines changed

7 files changed

+337
-0
lines changed

python-mixins/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# What Are Mixin Classes in Python?
2+
3+
This folder contains sample code for the Real Python tutorial [What Are Mixin Classes in Python?](https://realpython.com/python-mixin/).

python-mixins/mixins.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import json
2+
from typing import Self
3+
4+
5+
class SerializableMixin:
6+
def serialize(self) -> dict:
7+
if hasattr(self, "__slots__"):
8+
return {name: getattr(self, name) for name in self.__slots__}
9+
else:
10+
return vars(self)
11+
12+
13+
class JSONSerializableMixin:
14+
@classmethod
15+
def from_json(cls, json_string: str) -> Self:
16+
return cls(**json.loads(json_string))
17+
18+
def as_json(self) -> str:
19+
return json.dumps(vars(self))
20+
21+
22+
class TypedKeyMixin:
23+
def __init__(self, key_type, *args, **kwargs):
24+
super().__init__(*args, **kwargs)
25+
self.__type = key_type
26+
27+
def __setitem__(self, key, value):
28+
if not isinstance(key, self.__type):
29+
raise TypeError(f"key must be {self.__type} but was {type(key)}")
30+
super().__setitem__(key, value)
31+
32+
33+
class TypedValueMixin:
34+
def __init__(self, value_type, *args, **kwargs):
35+
super().__init__(*args, **kwargs)
36+
self.__type = value_type
37+
38+
def __setitem__(self, key, value):
39+
if not isinstance(value, self.__type):
40+
if not isinstance(value, self.__type):
41+
raise TypeError(
42+
f"value must be {self.__type} but was {type(value)}"
43+
)
44+
super().__setitem__(key, value)
45+
46+
47+
if __name__ == "__main__":
48+
from collections import UserDict
49+
from dataclasses import dataclass
50+
from pathlib import Path
51+
from types import SimpleNamespace
52+
53+
@dataclass
54+
class User(JSONSerializableMixin):
55+
user_id: int
56+
email: str
57+
58+
class AppSettings(JSONSerializableMixin, SimpleNamespace):
59+
def save(self, filepath: str | Path) -> None:
60+
Path(filepath).write_text(self.as_json(), encoding="utf-8")
61+
62+
class Inventory(TypedKeyMixin, TypedValueMixin, UserDict):
63+
pass
64+
65+
user = User(555, "[email protected]")
66+
print(user.as_json())
67+
68+
settings = AppSettings()
69+
settings.host = "localhost"
70+
settings.port = 8080
71+
settings.debug_mode = True
72+
settings.log_file = None
73+
settings.urls = (
74+
"https://192.168.1.200:8000",
75+
"https://192.168.1.201:8000",
76+
)
77+
settings.save("settings.json")
78+
79+
fruits = Inventory(str, int)
80+
fruits["apples"] = 42
81+
82+
try:
83+
fruits["🍌".encode("utf-8")] = 15
84+
except TypeError as ex:
85+
print(ex)
86+
87+
try:
88+
fruits["bananas"] = 3.5
89+
except TypeError as ex:
90+
print(ex)

python-mixins/stateful_v1.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
class TypedKeyMixin:
2+
key_type = object
3+
4+
def __setitem__(self, key, value):
5+
if not isinstance(key, self.key_type):
6+
raise TypeError(f"key must be {self.key_type} but was {type(key)}")
7+
super().__setitem__(key, value)
8+
9+
10+
class TypedValueMixin:
11+
value_type = object
12+
13+
def __setitem__(self, key, value):
14+
if not isinstance(value, self.value_type):
15+
raise TypeError(
16+
f"value must be {self.value_type} but was {type(value)}"
17+
)
18+
super().__setitem__(key, value)
19+
20+
21+
if __name__ == "__main__":
22+
from collections import UserDict
23+
24+
class Inventory(TypedKeyMixin, TypedValueMixin, UserDict):
25+
key_type = str
26+
value_type = int
27+
28+
fruits = Inventory()
29+
fruits["apples"] = 42
30+
31+
try:
32+
fruits["🍌".encode("utf-8")] = 15
33+
except TypeError as ex:
34+
print(ex)
35+
36+
try:
37+
fruits["bananas"] = 3.5
38+
except TypeError as ex:
39+
print(ex)
40+
41+
print(f"{vars(fruits) = }")

python-mixins/stateful_v2.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
def TypedKeyMixin(key_type=object):
2+
class _:
3+
def __setitem__(self, key, value):
4+
if not isinstance(key, key_type):
5+
raise TypeError(f"key must be {key_type} but was {type(key)}")
6+
super().__setitem__(key, value)
7+
8+
return _
9+
10+
11+
def TypedValueMixin(value_type=object):
12+
class _:
13+
def __setitem__(self, key, value):
14+
if not isinstance(value, value_type):
15+
raise TypeError(
16+
f"value must be {value_type} but was {type(value)}"
17+
)
18+
super().__setitem__(key, value)
19+
20+
return _
21+
22+
23+
if __name__ == "__main__":
24+
from collections import UserDict
25+
26+
class Inventory(TypedKeyMixin(str), TypedValueMixin(int), UserDict):
27+
key_type = "This attribute has nothing to collide with"
28+
29+
fruits = Inventory()
30+
fruits["apples"] = 42
31+
32+
try:
33+
fruits["🍌".encode("utf-8")] = 15
34+
except TypeError as ex:
35+
print(ex)
36+
37+
try:
38+
fruits["bananas"] = 3.5
39+
except TypeError as ex:
40+
print(ex)
41+
42+
print(f"{vars(fruits) = }")
43+
print(f"{Inventory.key_type = }")

python-mixins/stateful_v3.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
def key_type(expected_type):
2+
def class_decorator(cls):
3+
setitem = cls.__setitem__
4+
5+
def __setitem__(self, key, value):
6+
if not isinstance(key, expected_type):
7+
raise TypeError(
8+
f"key must be {expected_type} but was {type(key)}"
9+
)
10+
return setitem(self, key, value)
11+
12+
cls.__setitem__ = __setitem__
13+
return cls
14+
15+
return class_decorator
16+
17+
18+
def value_type(expected_type):
19+
def class_decorator(cls):
20+
setitem = cls.__setitem__
21+
22+
def __setitem__(self, key, value):
23+
if not isinstance(value, expected_type):
24+
raise TypeError(
25+
f"value must be {expected_type} but was {type(value)}"
26+
)
27+
return setitem(self, key, value)
28+
29+
cls.__setitem__ = __setitem__
30+
return cls
31+
32+
return class_decorator
33+
34+
35+
if __name__ == "__main__":
36+
from collections import UserDict
37+
38+
@key_type(str)
39+
@value_type(int)
40+
class Inventory(UserDict):
41+
key_type = "This attribute has nothing to collide with"
42+
43+
fruits = Inventory()
44+
fruits["apples"] = 42
45+
46+
try:
47+
fruits["🍌".encode("utf-8")] = 15
48+
except TypeError as ex:
49+
print(ex)
50+
51+
try:
52+
fruits["bananas"] = 3.5
53+
except TypeError as ex:
54+
print(ex)
55+
56+
print(f"{vars(fruits) = }")
57+
print(f"{Inventory.key_type = }")

python-mixins/typed_dict.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
def typed_dict(key_type=object, value_type=object):
2+
def class_decorator(cls):
3+
setitem = cls.__setitem__
4+
5+
def __setitem__(self, key, value):
6+
if not isinstance(key, key_type):
7+
raise TypeError(
8+
f"value must be {key_type} but was {type(key)}"
9+
)
10+
11+
if not isinstance(value, value_type):
12+
raise TypeError(
13+
f"value must be {value_type} but was {type(value)}"
14+
)
15+
16+
setitem(self, key, value)
17+
18+
cls.__setitem__ = __setitem__
19+
20+
return cls
21+
22+
return class_decorator
23+
24+
25+
if __name__ == "__main__":
26+
from collections import UserDict
27+
28+
# Enforce str keys and int values:
29+
@typed_dict(str, int)
30+
class Inventory(UserDict):
31+
pass
32+
33+
# Enforce str keys, allow any value type:
34+
@typed_dict(str)
35+
class AppSettings(UserDict):
36+
pass
37+
38+
fruits = Inventory()
39+
fruits["apples"] = 42
40+
41+
try:
42+
fruits["🍌".encode("utf-8")] = 15
43+
except TypeError as ex:
44+
print(ex)
45+
46+
try:
47+
fruits["bananas"] = 3.5
48+
except TypeError as ex:
49+
print(ex)
50+
51+
settings = AppSettings()
52+
settings["host"] = "localhost"
53+
settings["port"] = 8080
54+
settings["debug_mode"] = True
55+
56+
try:
57+
settings[b"binary data"] = "nope"
58+
except TypeError as ex:
59+
print(ex)

python-mixins/utils.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from collections import UserDict
2+
3+
4+
class DebugMixin:
5+
def __setitem__(self, key, value):
6+
super().__setitem__(key, value)
7+
print(f"Item set: {key=!r}, {value=!r}")
8+
9+
def __delitem__(self, key):
10+
super().__delitem__(key)
11+
print(f"Item deleted: {key=!r}")
12+
13+
14+
class CaseInsensitiveDict(DebugMixin, UserDict):
15+
def __setitem__(self, key: str, value: str) -> None:
16+
super().__setitem__(key.lower(), value)
17+
18+
def __getitem__(self, key: str) -> str:
19+
return super().__getitem__(key.lower())
20+
21+
def __delitem__(self, key: str) -> None:
22+
super().__delitem__(key.lower())
23+
24+
def __contains__(self, key: str) -> bool:
25+
return super().__contains__(key.lower())
26+
27+
def get(self, key: str, default: str = "") -> str:
28+
return super().get(key.lower(), default)
29+
30+
31+
if __name__ == "__main__":
32+
from pprint import pp
33+
34+
headers = CaseInsensitiveDict()
35+
headers["Content-Type"] = "application/json"
36+
headers["Cookie"] = "csrftoken=a4f3c7d28c194e5b; sessionid=f92e4b7c6"
37+
38+
print(f"{headers["cookie"] = }")
39+
print(f"{"CooKIE" in headers = }")
40+
41+
del headers["Cookie"]
42+
print(f"{headers = }")
43+
44+
pp(CaseInsensitiveDict.__mro__)

0 commit comments

Comments
 (0)