Layouts

Some websites serve multiple layouts for the same type of page.

For example, a product page may have different layouts for different product categories, and web-sites sometimes undergo A-B tests where the same page can have multiple variations.

In these cases, a good approach is to define one page object per layout, and then define a main page object that picks the right layout for the current response.

To avoid writing field-forwarding boilerplate in the main page object, use the layout_switch() decorator.

Basic usage

With layout_switch(), your main page object:

  • declares layout page objects as inputs, and

  • defines a switch method that returns the selected layout page object.

For example:

import attrs

from web_poet import HttpResponse, ItemPage, WebPage, field, layout_switch


@attrs.define
class Product:
    title: str
    price: str


class ProductLayoutA(WebPage[Product]):
    @field
    def title(self):
        return self.css("h1::text").get()

    @field
    def price(self):
        return self.css(".price::text").get()


class ProductLayoutB(WebPage[Product]):
    @field
    def title(self):
        return self.css(".title::text").get()


@layout_switch()
@attrs.define
class ProductPage(ItemPage[Product]):
    response: HttpResponse
    layout_a: ProductLayoutA
    layout_b: ProductLayoutB

    def get_layout(self) -> ItemPage[Product]:
        if self.response.css(".layout-a"):
            return self.layout_a
        return self.layout_b

    @field
    def price(self):
        return "N/A"

Field forwarding rules

By default, layout_switch() forwards fields based on the output item type field names.

This means that for item classes with declared fields (for example attrs, dataclasses, or pydantic models), the forwarded field set matches the item schema.

For each forwarded field:

  • if the selected layout defines the field, that layout field is used, and

  • if the selected layout does not define the field, a same-name field in the main page object is used as fallback.

For output item types that do not expose field names (for example dict), pass layout classes explicitly:

@layout_switch(layouts=[ProductLayoutA, ProductLayoutB])
@attrs.define
class ProductPage(ItemPage[dict]): ...

When layouts is provided, layout_switch() forwards the union of fields defined across those layout classes.