PEP 557*

versus the world

* Data Classes

Guillaume Gelin 

ramnes.eu 🇪🇺 🇫🇷

Let's go back in time...

collections.namedtuple

>>> from collections import namedtuple
>>> 
>>> InventoryItem = namedtuple("InventoryItem",
...                            ["name", "unit_price", "quantity"])
...                            
>>> item = InventoryItem("hammer", 10.49, 12)
>>> total_cost = item.unit_price * item.quantity
>>> total_cost
125.88

Properties? Defaults?

typing.NamedTuple

>>> from typing import NamedTuple
>>> 
>>> class InventoryItem(NamedTuple):
...     name: str
...     unit_price: float
...     quantity: int = 0
... 
...     @property
...     def  total_cost(self) -> float:
...         return self.unit_price * self.quantity
...     
>>> item = InventoryItem("hammer", 10.49, 12)
>>> item.total_cost
125.88

Mutable defaults?

...     
...     def __new__(cls, name, unit_price, quantity=0, related_items=None):
...         if related_items is None:
...             related_items = []
...         return super().__new__(cls, name, unit_price, quantity, related_items)
...     
$ python smartnamedtuple.py 
Traceback (most recent call last):
  File "smartnamedtuple.py", line 4, in <module>
    class InventoryItem(NamedTuple):
  File "/usr/lib64/python3.6/typing.py", line 2163, in __new__
    raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
AttributeError: Cannot overwrite NamedTuple attribute __new__
>>> from typing import NamedTuple, List
>>> 
>>> class InventoryItem(NamedTuple):
...     name: str
...     unit_price: float
...     quantity: int = 0
...     related_items: List = None
...     
...     def __real_new__(cls, name, unit_price, quantity=0, related_items=None):
...         if related_items is None:
...             related_items = []
...         return tuple.__new__(cls, [name, unit_price, quantity, related_items])
...     
>>> InventoryItem.__new__ = InventoryItem.__real_new__
>>> 
>>> item = InventoryItem("hammer", 10.49, 12)
>>> item.related_items
[]

PEP 557

Python 3.6

$ pip install dataclasses

Eric V. Smith

trueblade.com 🇺🇸

  • NamedTuple-ish

  • Real defaults

  • Mutability

@dataclass

>>> from dataclasses import dataclass
>>> 
>>> @dataclass
... class InventoryItem:
...     name: str
...     unit_price: float
...     quantity: int = 0
... 
...     @property
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
...     
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.total_cost
125.88
                
  • __init__

  • __repr__

  • __str__

  • Explicit __hash__

  • Lots of metadata

>>> from dataclasses import dataclass, field
>>> from typing import List
>>> 
>>> @dataclass
... class InventoryItem:
...     name: str
...     unit_price: float
...     quantity: int = 0
...     related_items: List = field(default_factory=list)
... 
...     @property
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
...     
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.related_items
[]

Real defaults

__post_init__

>>> from dataclasses import dataclass, field
>>> from typing import List
>>> 
>>> @dataclass
... class InventoryItem:
...     name: str
...     unit_price: float
...     quantity: int = 0
...     related_items: List = field(default_factory=list)
...     total_cost: float = field(init=False)
... 
...     def __post_init__(self):
...         self.total_cost = self.unit_price * self.quantity
... 
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.total_cost
125.88
>>> from dataclasses import dataclass, field
>>> from typing import List
>>> 
>>> @dataclass(frozen=True)
... class InventoryItem:
...     name: str
...     unit_price: float
...     quantity: int = 0
... 
...     @property
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
...     
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.unit_price += 1
Traceback (most recent call last):
  ...
FrozenInstanceError: cannot assign to field 'unit_price'

Freeze!

frozen + __post_init__

>>> from dataclasses import dataclass, field
>>> from typing import List
>>> 
>>> @dataclass(frozen=True)
... class InventoryItem:
...     name: str
...     unit_price: float
...     quantity: int = 0
...     related_items: List = field(default_factory=list)
...     total_cost: float = field(init=False)
... 
...     def __post_init__(self):
...         total_cost = self.unit_price * self.quantity
...         object.__setattr__(self, "total_cost", total_cost)
... 
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.total_cost
125.88

Hash is key

>>> from dataclasses import dataclass, field
>>> from typing import List
>>> 
>>> @dataclass(unsafe_hash=True)
... class InventoryItem:
...     name: str
...     unit_price: float
...     related_items: List = field(default_factory=list,
...                                 hash=False)
... 
>>> item1 = InventoryItem("hammer", 10.4)
>>> item2 = InventoryItem("hammer", 10.4)
>>> item3 = InventoryItem("spanner", 8.9)
>>> {item1, item2, item3}
{InventoryItem(name='hammer', ...),
 InventoryItem(name='spanner', ...)}

@dataclass + mypy = ❤️

from dataclasses import dataclass, field

@dataclass
class InventoryItem:
    name: str
    unit_price: float
    quantity: int = 0
    related_items: List = field(default_factory=list)

item = InventoryItem("what", "are", "you", "doing?")

wtf.py

$ mypy wtf.py
wtf.py:11: error: Argument 2 to "InventoryItem" has incompatible type "str"; expected "float"
wtf.py:11: error: Argument 3 to "InventoryItem" has incompatible type "str"; expected "int"
wtf.py:11: error: Argument 4 to "InventoryItem" has incompatible type "str"; expected "List[Any]"

namedtuple returns

>>> from dataclasses import field, make_dataclass
>>> from typing import List
>>> 
>>> make_dataclass("InventoryItem",
...                [("name", str),
...                 ("unit_price", float),
...                 ("quantity", int, 0),
...                 ("related_items", List,
...                  field(default_factory=list))])
...                            
types.InventoryItem
>>> item = _("hammers", 10.49, 12)
>>> item.unit_price
10.49
>>> item.related_items
[]
>>> from dataclasses import asdict, astuple
>>> 
>>> item = InventoryItem("hammers", 10.49, 12)
>>> 
>>> asdict(item)
{'name': 'hammers', 'unit_price': 10.49, 'quantity': 12,
 'related_items': []}
>>> 
>>> astuple(item)
('hammers', 10.49, 12, [])

Let's go back in time...

(again)

attrs

Python 2.7 and 3.4+

$ pip install attrs

Hynek Schlawack 

hynek.me 🇪🇺 🇩🇪

@attr.s

>>> import attr
>>> 
>>> @attr.s
... class InventoryItem:
...     name: str = attr.ib()
...     unit_price: float = attr.ib()
...     quantity: int = attr.ib(default=0)
... 
...     @property
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
...     
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.total_cost
125.88
  • __init__

  • __repr__

  • __str__

  • __eq__

  • __hash__

  • __ne__

  • __lt__

  • __le__

  • __gt__

  • __ge__

>>> @attr.s
... class InventoryItem:
...     price: float = attr.ib()
... 
...     @price.validator
...     def check(self, attribute, value):
...         if value > 9000:
...             raise ValueError("Dude? That's too expensive!")
... 
>>> @attr.s
... class InventoryItem:
...     price: float = attr.ib(
...         validators=attr.validators.instance_of(float)
...     )
... 
>>> @attr.s
... class InventoryItem:
...     price: Any = attr.ib(converter=float)
... 
>>> InventoryItem("10.49")
InventoryItem(price=10.49)

So...

NoSQL?

The MongoDB example

>>> from pymongo import MongoClient
>>> 
>>> client = MongoClient()
>>> database = client.amazing
>>> 
>>> item_dict = database.inventory_items.find_one()
>>> item_dict
{'name': 'hammers', 'price': 10.49, 'quantity': 10}
  • Real OOP types

  • Properties

  • Dot notation

dict

>>> class InventoryItem(dict):
... 
...     @property
...     def total_cost(self) -> float:
...         return self["unit_price"] * self['quantity']
... 
>>> item = InventoryItem(item_dict)
>>> item.total_cost
125.88

types.SimpleNamespace

>>> from types import SimpleNamespace
>>> 
>>> class InventoryItem(SimpleNamespace):
... 
...     @property
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
... 
>>> item = InventoryItem(**item_dict)
>>> item.total_cost
125.88
>>> from copy import copy
>>> 
>>> copy(item.__dict__)
{'name': 'hammers', 'price': 10.49, 'quantity': 10}

box

>>> from box import Box
>>> 
>>> class InventoryItem(Box):
... 
...     @property
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
... 
>>> item = InventoryItem(item_dict)
>>> item.name
'hammer'
>>> item.total_cost
125.88
>>> item.to_dict()
{'name': 'hammer', 'unit_price': 10.49, 'quantity': 12}

51K instantiations + item access  

Thingy

Python 2.7 and 3.4+

$ pip install Thingy

Thingy

>>> class InventoryItem(Thingy):
... 
...     @property
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
... 
>>> item = InventoryItem(item_dict)
>>> item.total_cost
125.88
>>> item.view()
{'name': 'hammer', 'unit_price': 10.49, 'quantity': 12}

???

>>> class InventoryItem(Thingy):
... 
...     @property
...     def total_cost(self) -> float:
...         return self.unit_price * self.quantity
... 
>>> InventoryItem.add_view("with total", include="total_cost")
>>> 
>>> item = InventoryItem(item_dict)
>>> item.view("with total")
{'name': 'hammer', 'unit_price': 10.49, 'quantity': 12,
 'total_cost': 125.88}

Also availabe in

MongoDB flavor

Self-Q&A

Does @dataclass deprecate
Thingy?

Does it deprecate
named tuples?

(whatever module they're from)

Does it deprecate
​SQLAlchemy?

Does it deprecate
​Marshmallow?

Does it deprecate...

class itself?

Thank you!