2 minutes
Dataclasses: No More Boilerplate
Welcome to part six of our Python 3 features series! Today we’re looking at two gems: dataclasses
for boilerplate-free data containers, and contextvars
for managing context in async code.
1. Dataclasses
Before Python 3.7, you’d write classes like this if you wanted to bundle data:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
That’s a lot of typing for two fields. Enter dataclasses
(PEP 557):
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(3.5, 7.2)
print(p) # Point(x=3.5, y=7.2)
All of that __init__
, __repr__
, and comparison methods come for free.
Handy Features
Default values:
@dataclass class User: name: str active: bool = True
Immutability with
frozen=True
:@dataclass(frozen=True) class Color: r: int g: int b: int c = Color(255, 0, 0) # c.r = 128 # ❌ can't change!
Field customization with
field()
:from dataclasses import field @dataclass class Item: name: str price: float = field(default=0.0, repr=False)
Convert to dict easily:
from dataclasses import asdict print(asdict(p)) # {'x': 3.5, 'y': 7.2}
2. Context Variables: Keeping State in Async Land
When you run async tasks in parallel, global vars can clash. contextvars
(PEP 567) fixes that by giving each task its own context.
import asyncio
from contextvars import ContextVar
current_user: ContextVar[str] = ContextVar('current_user')
async def handle_request(name):
token = current_user.set(name)
await asyncio.sleep(0.1)
print(f"Handling request for {current_user.get()}")
current_user.reset(token)
async def main():
await asyncio.gather(
handle_request('alice'),
handle_request('bob'),
)
asyncio.run(main())
# Handling request for alice
# Handling request for bob
Each coroutine sees its own current_user
without stomping on others.
That’s it for dataclasses
and contextvars
. In our final part, we’ll explore the walrus operator and positional-only params to wrap up this series. Happy coding!