Skip to content

Materials for Python Mixins #684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions python-mixins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# What Are Mixin Classes in Python?

This folder contains sample code for the Real Python tutorial [What Are Mixin Classes in Python?](https://realpython.com/python-mixin/).
90 changes: 90 additions & 0 deletions python-mixins/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import json
from typing import Self


class SerializableMixin:
def serialize(self) -> dict:
if hasattr(self, "__slots__"):
return {name: getattr(self, name) for name in self.__slots__}
else:
return vars(self)


class JSONSerializableMixin:
@classmethod
def from_json(cls, json_string: str) -> Self:
return cls(**json.loads(json_string))

def as_json(self) -> str:
return json.dumps(vars(self))


class TypedKeyMixin:
def __init__(self, key_type, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__type = key_type

def __setitem__(self, key, value):
if not isinstance(key, self.__type):
raise TypeError(f"key must be {self.__type} but was {type(key)}")
super().__setitem__(key, value)


class TypedValueMixin:
def __init__(self, value_type, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__type = value_type

def __setitem__(self, key, value):
if not isinstance(value, self.__type):
if not isinstance(value, self.__type):
raise TypeError(
f"value must be {self.__type} but was {type(value)}"
)
super().__setitem__(key, value)


if __name__ == "__main__":
from collections import UserDict
from dataclasses import dataclass
from pathlib import Path
from types import SimpleNamespace

@dataclass
class User(JSONSerializableMixin):
user_id: int
email: str

class AppSettings(JSONSerializableMixin, SimpleNamespace):
def save(self, filepath: str | Path) -> None:
Path(filepath).write_text(self.as_json(), encoding="utf-8")

class Inventory(TypedKeyMixin, TypedValueMixin, UserDict):
pass

user = User(555, "[email protected]")
print(user.as_json())

settings = AppSettings()
settings.host = "localhost"
settings.port = 8080
settings.debug_mode = True
settings.log_file = None
settings.urls = (
"https://192.168.1.200:8000",
"https://192.168.1.201:8000",
)
settings.save("settings.json")

fruits = Inventory(str, int)
fruits["apples"] = 42

try:
fruits["🍌".encode("utf-8")] = 15
except TypeError as ex:
print(ex)

try:
fruits["bananas"] = 3.5
except TypeError as ex:
print(ex)
41 changes: 41 additions & 0 deletions python-mixins/stateful_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class TypedKeyMixin:
key_type = object

def __setitem__(self, key, value):
if not isinstance(key, self.key_type):
raise TypeError(f"key must be {self.key_type} but was {type(key)}")
super().__setitem__(key, value)


class TypedValueMixin:
value_type = object

def __setitem__(self, key, value):
if not isinstance(value, self.value_type):
raise TypeError(
f"value must be {self.value_type} but was {type(value)}"
)
super().__setitem__(key, value)


if __name__ == "__main__":
from collections import UserDict

class Inventory(TypedKeyMixin, TypedValueMixin, UserDict):
key_type = str
value_type = int

fruits = Inventory()
fruits["apples"] = 42

try:
fruits["🍌".encode("utf-8")] = 15
except TypeError as ex:
print(ex)

try:
fruits["bananas"] = 3.5
except TypeError as ex:
print(ex)

print(f"{vars(fruits) = }")
43 changes: 43 additions & 0 deletions python-mixins/stateful_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
def TypedKeyMixin(key_type=object):
class _:
def __setitem__(self, key, value):
if not isinstance(key, key_type):
raise TypeError(f"key must be {key_type} but was {type(key)}")
super().__setitem__(key, value)

return _


def TypedValueMixin(value_type=object):
class _:
def __setitem__(self, key, value):
if not isinstance(value, value_type):
raise TypeError(
f"value must be {value_type} but was {type(value)}"
)
super().__setitem__(key, value)

return _


if __name__ == "__main__":
from collections import UserDict

class Inventory(TypedKeyMixin(str), TypedValueMixin(int), UserDict):
key_type = "This attribute has nothing to collide with"

fruits = Inventory()
fruits["apples"] = 42

try:
fruits["🍌".encode("utf-8")] = 15
except TypeError as ex:
print(ex)

try:
fruits["bananas"] = 3.5
except TypeError as ex:
print(ex)

print(f"{vars(fruits) = }")
print(f"{Inventory.key_type = }")
57 changes: 57 additions & 0 deletions python-mixins/stateful_v3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
def key_type(expected_type):
def class_decorator(cls):
setitem = cls.__setitem__

def __setitem__(self, key, value):
if not isinstance(key, expected_type):
raise TypeError(
f"key must be {expected_type} but was {type(key)}"
)
return setitem(self, key, value)

cls.__setitem__ = __setitem__
return cls

return class_decorator


def value_type(expected_type):
def class_decorator(cls):
setitem = cls.__setitem__

def __setitem__(self, key, value):
if not isinstance(value, expected_type):
raise TypeError(
f"value must be {expected_type} but was {type(value)}"
)
return setitem(self, key, value)

cls.__setitem__ = __setitem__
return cls

return class_decorator


if __name__ == "__main__":
from collections import UserDict

@key_type(str)
@value_type(int)
class Inventory(UserDict):
key_type = "This attribute has nothing to collide with"

fruits = Inventory()
fruits["apples"] = 42

try:
fruits["🍌".encode("utf-8")] = 15
except TypeError as ex:
print(ex)

try:
fruits["bananas"] = 3.5
except TypeError as ex:
print(ex)

print(f"{vars(fruits) = }")
print(f"{Inventory.key_type = }")
59 changes: 59 additions & 0 deletions python-mixins/typed_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
def typed_dict(key_type=object, value_type=object):
def class_decorator(cls):
setitem = cls.__setitem__

def __setitem__(self, key, value):
if not isinstance(key, key_type):
raise TypeError(
f"value must be {key_type} but was {type(key)}"
)

if not isinstance(value, value_type):
raise TypeError(
f"value must be {value_type} but was {type(value)}"
)

setitem(self, key, value)

cls.__setitem__ = __setitem__

return cls

return class_decorator


if __name__ == "__main__":
from collections import UserDict

# Enforce str keys and int values:
@typed_dict(str, int)
class Inventory(UserDict):
pass

# Enforce str keys, allow any value type:
@typed_dict(str)
class AppSettings(UserDict):
pass

fruits = Inventory()
fruits["apples"] = 42

try:
fruits["🍌".encode("utf-8")] = 15
except TypeError as ex:
print(ex)

try:
fruits["bananas"] = 3.5
except TypeError as ex:
print(ex)

settings = AppSettings()
settings["host"] = "localhost"
settings["port"] = 8080
settings["debug_mode"] = True

try:
settings[b"binary data"] = "nope"
except TypeError as ex:
print(ex)
44 changes: 44 additions & 0 deletions python-mixins/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from collections import UserDict


class DebugMixin:
def __setitem__(self, key, value):
super().__setitem__(key, value)
print(f"Item set: {key=!r}, {value=!r}")

def __delitem__(self, key):
super().__delitem__(key)
print(f"Item deleted: {key=!r}")


class CaseInsensitiveDict(DebugMixin, UserDict):
def __setitem__(self, key: str, value: str) -> None:
super().__setitem__(key.lower(), value)

def __getitem__(self, key: str) -> str:
return super().__getitem__(key.lower())

def __delitem__(self, key: str) -> None:
super().__delitem__(key.lower())

def __contains__(self, key: str) -> bool:
return super().__contains__(key.lower())

def get(self, key: str, default: str = "") -> str:
return super().get(key.lower(), default)


if __name__ == "__main__":
from pprint import pp

headers = CaseInsensitiveDict()
headers["Content-Type"] = "application/json"
headers["Cookie"] = "csrftoken=a4f3c7d28c194e5b; sessionid=f92e4b7c6"

print(f"{headers["cookie"] = }")
print(f"{"CooKIE" in headers = }")

del headers["Cookie"]
print(f"{headers = }")

pp(CaseInsensitiveDict.__mro__)