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!

PEP 557 versus the world

By Guillaume Gelin

PEP 557 versus the world

Python 3.7 will ship with a new module called "dataclasses", which has been defined in PEP 557. What is this module? What are the problems that PEP 557 authors try to solve? What was the chosen design, and why? How does it compare against the tools that already exist?

  • 1,566