Last modified: Aug 31, 2026

attrs BeautifulSoup: Clean & Fast Scraping

Web scraping with BeautifulSoup is powerful. But raw code often becomes messy. You parse HTML, extract data, and store it in dictionaries. This works for small projects. However, it gets hard to manage as your project grows.

This is where attrs comes in. It helps you define structured data classes. Combined with BeautifulSoup, you get clean, readable, and maintainable code. This guide will show you how to use them together effectively.

Why Use attrs with BeautifulSoup?

BeautifulSoup is great for navigating HTML. But it returns unstructured data. You often end up with a list of dictionaries. Dictionaries are flexible but lack structure. Typos in keys can cause silent errors.

attrs solves this by letting you define classes with typed attributes. You get auto-generated __init__, __repr__, and comparison methods. This makes your scraping code more robust and self-documenting.

Using attrs also improves performance. It uses a slotted class implementation by default. This reduces memory usage and speeds up attribute access. For large scraping tasks, this can be a significant advantage.

Installing Required Libraries

First, you need to install the necessary packages. Use pip to install beautifulsoup4 and attrs. If you haven't set up your environment, check out this guide on Anaconda BeautifulSoup: Install & Scrape Guide.


pip install beautifulsoup4 attrs requests

We also install requests to fetch web pages. This is a standard setup for most scraping projects. Once installed, you are ready to start coding.

Basic Example: Scraping Product Data

Let's start with a simple example. We will scrape product names and prices from a mock HTML page. This will demonstrate the core concept of combining attrs and BeautifulSoup.


from bs4 import BeautifulSoup
from attrs import define, field
import requests

# Define an attrs class for our data
@define
class Product:
    name: str
    price: float

# Sample HTML (in real life, you would fetch this)
html_doc = """

Laptop

$999.99

Mouse

$19.50
""" soup = BeautifulSoup(html_doc, 'html.parser') products = [] # Parse the HTML and create Product objects for item in soup.find_all('div', class_='product'): name = item.find('h2', class_='name').text price_text = item.find('span', class_='price').text price = float(price_text.replace('$', '')) products.append(Product(name=name, price=price)) # Print the results for product in products: print(product)

Product(name='Laptop', price=999.99)
Product(name='Mouse', price=19.5)

Notice how clean the output is. The __repr__ method is automatically generated by attrs. This makes debugging much easier. You can see all attributes of the object at a glance.

Adding Validation with attrs Validators

One of the best features of attrs is validators. You can validate data as it is assigned. This is crucial when scraping, as web data can be messy. Let's add a validator to ensure prices are always positive.


from attrs import define, field, validators

@define
class Product:
    name: str
    price: float = field(validator=validators.gt(0))

# This will work fine
p1 = Product(name='Keyboard', price=45.00)
print(p1)

# This will raise a ValueError
try:
    p2 = Product(name='Broken Item', price=-5.00)
except ValueError as e:
    print(f"Error: {e}")

Product(name='Keyboard', price=45.0)
Error: price must be > 0: -5.0

Validators add a safety net to your code. They catch bad data early. This prevents errors from propagating through your entire pipeline. You can also use validators for type checks and custom logic.

For more advanced data extraction techniques, you might want to learn how to Assign Links with BeautifulSoup. This skill is essential for scraping navigation and related content.

Nested Data Structures with attrs

Real-world web pages are often nested. A product might have a category, which has a subcategory. attrs handles this elegantly. You can define nested classes to mirror the HTML structure.


from bs4 import BeautifulSoup
from attrs import define

@define
class Category:
    name: str
    id: int

@define
class Product:
    name: str
    price: float
    category: Category

html_doc = """

Smartphone

$699.00
Electronics
""" soup = BeautifulSoup(html_doc, 'html.parser') item = soup.find('div', class_='product') name = item.find('h2').text price = float(item.find('span', class_='price').text.replace('$', '')) cat_elem = item.find('div', class_='cat') category = Category(name=cat_elem.text, id=int(cat_elem['data-id'])) product = Product(name=name, price=price, category=category) print(product) print(f"Category: {product.category.name} (ID: {product.category.id})")

Product(name='Smartphone', price=699.0, category=Category(name='Electronics', id=5))
Category: Electronics (ID: 5)

Nested classes make your data model very clear. It maps directly to the HTML structure. This approach is much better than using nested dictionaries.

Performance Benefits of attrs Slots

By default, attrs creates slotted classes. Slots are a Python feature that prevents dynamic attribute creation. This makes attribute access faster and reduces memory overhead. For scraping thousands of pages, this matters.

Let's compare a regular class with an attrs class. We will measure the memory usage and speed. This will show you why attrs is a smart choice for large-scale scraping.


import tracemalloc
from attrs import define
import time

# Regular class
class RegularProduct:
    def __init__(self, name, price):
        self.name = name
        self.price = price

# attrs class
@define
class AttrsProduct:
    name: str
    price: float

# Test memory usage
tracemalloc.start()
regular_products = [RegularProduct(f"Item {i}", i * 1.5) for i in range(100000)]
current, peak = tracemalloc.get_traced_memory()
print(f"Regular class: {peak / 1024 / 1024:.2f} MB")
tracemalloc.stop()

tracemalloc.start()
attrs_products = [AttrsProduct(f"Item {i}", i * 1.5) for i in range(100000)]
current, peak = tracemalloc.get_traced_memory()
print(f"attrs class: {peak / 1024 / 1024:.2f} MB")
tracemalloc.stop()

Regular class: 24.51 MB
attrs class: 15.32 MB

As you can see, the attrs class uses significantly less memory. This is due to the slotted implementation. This efficiency becomes critical when scraping large websites.

Converting attrs Objects to Dictionaries

Sometimes you need to convert your attrs objects to dictionaries. This is useful for serialization, like converting to JSON or storing in a database. attrs provides a built-in method for this called asdict().


from attrs import define, asdict

@define
class Product:
    name: str
    price: float

product = Product(name='Monitor', price=249.99)
product_dict = asdict(product)
print(product_dict)

# You can easily convert to JSON
import json
json_data = json.dumps(product_dict)
print(json_data)

{'name': 'Monitor', 'price': 249.99}
{"name": "Monitor", "price": 249.99}

This conversion is seamless. It works with nested attrs objects too. This makes it easy to integrate with other parts of your data pipeline. For more advanced use cases, consider exploring BeautifulSoup Async: Speed Up Web Scraping.

Handling Missing Data Gracefully

Web pages often have missing elements. A product might be out of stock and not have a price. You need to handle these cases gracefully. attrs allows you to set default values for fields.


from bs4 import BeautifulSoup
from attrs import define, field

@define
class Product:
    name: str
    price: float = field(default=0.0)
    in_stock: bool = field(default=False)

html_doc = """

Out of Stock Item

""" soup = BeautifulSoup(html_doc, 'html.parser') item = soup.find('div', class_='product') name = item.find('h2').text # Check if price exists price_elem = item.find('span', class_='price') price = float(price_elem.text.replace('$', '')) if price_elem else 0.0 product = Product(name=name, price=price) print(product) print(f"In stock: {product.in_stock}")

Product(name='Out of Stock Item', price=0.0, in_stock=False)
In stock: False

Using defaults prevents your code from crashing. It also provides a clear representation of missing data. This is much better than getting a None value and having to check for it everywhere.

Comparing attrs with Dataclasses

Python's standard library has dataclasses. They serve a similar purpose to attrs. However, attrs offers more features out of the box. It has validators, converters, and more powerful field definitions.

attrs also has better performance. Dataclasses are not slotted by default. This makes attrs a better choice for performance-critical scraping tasks. If you are already using BeautifulSoup, adding attrs is a natural fit.

For a deeper understanding of how BeautifulSoup compares to other tools, check out this guide on BeautifulSoup vs Scrapy: The Ultimate Guide. It will help you choose the right tool for your project.

Conclusion

Combining attrs with BeautifulSoup transforms your web scraping code. It makes it cleaner, more reliable, and faster. The structured classes eliminate the mess of dictionaries. The validators catch bad data early. The slotted classes save memory.

Start using attrs in your next scraping project. You will notice the difference immediately. Your code will be easier to read, debug, and maintain. This combination is a best practice for serious web scraping with Python.

Remember to always respect the website's robots.txt and terms of service. Use these powerful tools responsibly. Happy scraping!