DEV Community

Cover image for Why should you use attrs more
Soumendra Kumar Sahoo
Soumendra Kumar Sahoo

Posted on • Originally published at soumendrak.com

Why should you use attrs more

Introduction

Python's attrs library is a game-changer for developers looking to simplify class creation and reduce boilerplate code. This libray is even trusted by NASA.
Created by Hynek Schlawack in 2015, attrs has quickly become a favorite tool among Python developers for its ability to automatically generate special methods and provide a clean, declarative way to define classes.
dataclasses is a kind of subset of attrs.

Why attrs is useful:

  • Reduces boilerplate code
  • Improves code readability and maintainability
  • Provides powerful features for data validation and conversion
  • Enhances performance through optimized implementations

2. Getting Started with attrs

Installation:
To get started with attrs, you can install it using pip:

pip install attrs
Enter fullscreen mode Exit fullscreen mode

Basic usage:
Here's a simple example of how to use attrs to define a class:

import attr

@attr.s
class Person:
    name = attr.ib()
    age = attr.ib()

# Creating an instance
person = Person("Alice", 30)
print(person)  # Person(name='Alice', age=30)
Enter fullscreen mode Exit fullscreen mode

3. Core Features of attrs

a. Automatic method generation:

attrs automatically generates init, repr, and eq methods for your classes:

@attr.s
class Book:
    title = attr.ib()
    author = attr.ib()
    year = attr.ib()

book1 = Book("1984", "George Orwell", 1949)
book2 = Book("1984", "George Orwell", 1949)

print(book1)  # Book(title='1984', author='George Orwell', year=1949)
print(book1 == book2)  # True
Enter fullscreen mode Exit fullscreen mode

b. Attribute definition with types and default values:

import attr
from typing import List

@attr.s
class Library:
    name = attr.ib(type=str)
    books = attr.ib(type=List[str], default=attr.Factory(list))
    capacity = attr.ib(type=int, default=1000)

library = Library("City Library")
print(library)  # Library(name='City Library', books=[], capacity=1000)
Enter fullscreen mode Exit fullscreen mode

c. Validators and converters:

import attr

def must_be_positive(instance, attribute, value):
    if value <= 0:
        raise ValueError("Value must be positive")

@attr.s
class Product:
    name = attr.ib()
    price = attr.ib(converter=float, validator=[attr.validators.instance_of(float), must_be_positive])

product = Product("Book", "29.99")
print(product)  # Product(name='Book', price=29.99)

try:
    Product("Invalid", -10)
except ValueError as e:
    print(e)  # Value must be positive
Enter fullscreen mode Exit fullscreen mode

4. Advanced Usage

a. Customizing attribute behavior:

import attr

@attr.s
class User:
    username = attr.ib()
    _password = attr.ib(repr=False)  # Exclude from repr

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, value):
        self._password = hash(value)  # Simple hashing for demonstration

user = User("alice", "secret123")
print(user)  # User(username='alice')
Enter fullscreen mode Exit fullscreen mode

b. Frozen instances and slots:

@attr.s(frozen=True) # slots=True is the default
class Point:
    x = attr.ib()
    y = attr.ib()

point = Point(1, 2)
try:
    point.x = 3  # This will raise an AttributeError
except AttributeError as e:
    print(e)  # can't set attribute
Enter fullscreen mode Exit fullscreen mode

c. Factory functions and post-init processing:

import attr
import uuid

@attr.s
class Order:
    id = attr.ib(factory=uuid.uuid4)
    items = attr.ib(factory=list)
    total = attr.ib(init=False)

    def __attrs_post_init__(self):
        self.total = sum(item.price for item in self.items)

@attr.s
class Item:
    name = attr.ib()
    price = attr.ib(type=float)

order = Order(items=[Item("Book", 10.99), Item("Pen", 1.99)])
print(order)  # Order(id=UUID('...'), items=[Item(name='Book', price=10.99), Item(name='Pen', price=1.99)], total=12.98)
Enter fullscreen mode Exit fullscreen mode

5. Best Practices and Common Pitfalls

Best Practices:

  • Use type annotations for better code readability and IDE support
  • Leverage validators for data integrity
  • Use frozen classes for immutable objects
  • Take advantage of automatic method generation to reduce code duplication

Common Pitfalls:

  • Forgetting to use @attr.s decorator on the class
  • Overusing complex validators that could be separate methods
  • Not considering the performance impact of extensive use of factory functions

6. attrs vs Other Libraries

Library Features Performance Community
attrs Automatic method generation, attribute definition with types and default values, validators and converters Better performance than manual code Active community
pydantic Data validation and settings management, automatic method generation, attribute definition with types and default values, validators and converters Good performance Active community
dataclasses Built into Python 3.7+, making them more accessible Tied to the Python version Built-in Python library

attrs and dataclasses are faster than pydantic1.

Comparison with dataclasses:

  • attrs is more feature-rich and flexible
  • dataclasses are built into Python 3.7+, making them more accessible
  • attrs has better performance in most cases
  • dataclasses are tied to the Python version, while attrs as an external library can be used with any Python version.

Comparison with pydantic:

  • pydantic is focused on data validation and settings management
  • attrs is more general-purpose and integrates better with existing codebases
  • pydantic has built-in JSON serialization, while attrs requires additional libraries

When to choose attrs:

  • For complex class hierarchies with custom behaviors
  • When you need fine-grained control over attribute definitions
  • For projects that require Python 2 compatibility (though less relevant now)

7. Performance and Real-world Applications

Performance:
attrs generally offers better performance than manually written classes or other libraries due to its optimized implementations.

Real-world example:

from attr import define, Factory
from typing import List, Optional

@define
class Customer:
    id: int
    name: str
    email: str
    orders: List['Order'] = Factory(list)

@define
class Order:
    id: int
    customer_id: int
    total: float
    items: List['OrderItem'] = Factory(list)

@define
class OrderItem:
    id: int
    order_id: int
    product_id: int
    quantity: int
    price: float

@define
class Product:
    id: int
    name: str
    price: float
    description: Optional[str] = None

# Usage
customer = Customer(1, "Alice", "alice@example.com")
product = Product(1, "Book", 29.99, "A great book")
order_item = OrderItem(1, 1, 1, 2, product.price)
order = Order(1, customer.id, 59.98, [order_item])
customer.orders.append(order)

print(customer)
Enter fullscreen mode Exit fullscreen mode

8. Conclusion and Call to Action

attrs is a powerful library that simplifies Python class definitions while providing robust features for data validation and manipulation. Its ability to reduce boilerplate code, improve readability, and enhance performance makes it an invaluable tool for Python developers.

Community resources:

Try attrs in your next project and experience its benefits firsthand. Share your experiences with the community and contribute to its ongoing development. Happy coding!


  1. https://stefan.sofa-rockers.org/2020/05/29/attrs-dataclasses-pydantic/ 

Top comments (2)

Collapse
 
sreno77 profile image
Scott Reno

Great post! I'd never heard of attrs lib before.

Collapse
 
soumendrak profile image
Soumendra Kumar Sahoo

Glad you like it. I have also come to know about attrs recently.