Python Playwright: iframe Tabs and Multiple Pages
python playwright iframe tabs and multiple pages: Handle iframes, tabs, and multiple pages in Python Playwright using frame_locator, expect_popup, and context.pages wi...
When you automate a browser with Python Playwright, tabs and iframes are not separate concepts that need special drivers. A tab is a Page object, and an iframe is a Frame that lives inside a page. The practical difficulty in python playwright iframe tabs and multiple pages work is knowing which API to use for each layer and how to wait for the right object to exist before interacting with it.
How Playwright Models Tabs, Pages, and Frames
Playwright organizes the browser into three levels: Browser, BrowserContext, and Page. A BrowserContext is an isolated browsing session, and every tab or window opened inside that context is a Page. When a link uses target="_blank" or JavaScript calls window.open(), the new tab is still a Page in the same context.
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() context = browser.new_context() page = context.new_page() page.goto("https://example.com") print(context.pages) # every open tab in this context browser.close()
Frames are a separate layer. Every page has a main frame, and iframes are child frames. Playwright exposes them through page.frames, page.main_frame, and frame_locator(). The key distinction: a Frame object represents the frame itself, while a FrameLocator is a lazy handle that resolves to elements inside that frame when you perform an action.
Locating Elements Inside an iframe
To interact with content inside an iframe, use frame_locator() with a CSS selector that targets the iframe element. The returned FrameLocator behaves like a regular locator, but all queries are scoped to the frame's document.
widget = page.frame_locator("#payment-widget") widget.locator("input[name='card-number']").fill("4242424242424242") widget.locator("button[type='submit']").click()
The selector passed to frame_locator() must match the <iframe> element itself, not content inside it. If the iframe is added dynamically, Playwright's auto-waiting retries the frame lookup until the iframe appears, so you do not need a manual sleep before calling frame_locator().
An alternative is to grab the frame object directly from a locator:
iframe_element = page.locator("iframe[data-widget='chat']") frame = iframe_element.content_frame frame.locator("textarea").fill("Hello")
content_frame returns the Frame associated with that iframe element, or None if the frame has not loaded. Use frame_locator() when you only need to act on elements inside the frame, and use content_frame when you need frame properties such as frame.url or frame.title.
Handling Nested iframes
Iframes can contain other iframes. FrameLocator supports chaining so you can descend through each level without resolving intermediate frame objects.
outer = page.frame_locator("#outer-frame") inner = outer.frame_locator("#inner-frame") inner.locator("button.confirm").click()
Each call to frame_locator() narrows the search to the next frame level. The chain is resolved lazily at action time, which means the whole chain is re-evaluated if the page changes between steps. This is useful for single-page applications that re-render frames after navigation.
Working with Multiple Tabs and Pages
context.pages gives you the current list of open pages in the context. When a tab closes, it is removed from that list. To switch the active tab, call page.bring_to_front() on the page you want to focus.
for pg in context.pages: print(pg.url) context.pages[-1].bring_to_front()
Do not rely on context.pages ordering for correctness. The order reflects the order pages were opened, but a page that was opened earlier may close later, and new pages are appended as they appear. If you need a specific page, identify it by URL or by a locator that is unique to that tab.
Capturing Popups and New Tabs During Actions
When a click or navigation opens a new tab, Playwright provides expect_popup() on the page and expect_page() on the context. Both are context managers that wait for the new page to appear while the triggering action runs.
with context.expect_page() as new_page_info: page.click("a[target='_blank']") new_page = new_page_info.value new_page.wait_for_load_state("domcontentloaded") print(new_page.title())
Use expect_popup() when the new tab is opened directly from the current page, and expect_page() when the new page might be created by any action in the context, such as a service worker or a delayed window.open(). The context manager blocks until the page appears or the timeout expires, so it is safer than polling context.pages manually.
Combining iframe Interaction with Multi-Page Flows
A common scenario is opening a new tab and then interacting with an iframe inside that tab. The two features compose naturally because a FrameLocator is always created from a specific page.
with context.expect_page() as new_page_info: page.click("button#open-checkout") checkout = new_page_info.value checkout.wait_for_load_state("networkidle") card_form = checkout.frame_locator("#card-widget") card_form.locator("input[name='cvv']").fill("123") card_form.locator("button.pay").click()
The frame locator is bound to checkout, not to the original page, so the lookup never accidentally resolves against the wrong tab. If the iframe is inside a popup window, the same pattern works because the popup is also a Page in the same context.
Waiting, Timeouts, and Runtime Behavior
Playwright auto-waits for elements to be actionable before clicking or filling, and frame_locator() participates in that waiting. The main runtime cost in multi-page work is the time spent waiting for new pages and frames to reach the state you need. wait_for_load_state() accepts "domcontentloaded", "load", and "networkidle". Prefer "domcontentloaded" when you only need the DOM, because "networkidle" can wait several seconds on pages with long-lived connections.
Set a reasonable timeout on expect_page() and expect_popup() when the new tab is not guaranteed to open:
with context.expect_page(timeout=5000) as new_page_info: page.click("button#maybe-opens-tab")
If the timeout is too short, the action may still succeed but the page object is never captured. If the page opens after the timeout, it remains in context.pages and you can recover by searching that list.
Common Failure Modes and Compatibility Notes
A frame locator fails when the iframe is removed from the DOM before the action runs. Because FrameLocator re-resolves at action time, a frame that was present during a previous step can disappear after navigation, producing a timeout rather than a stale-element error. Re-query the frame after navigation instead of caching a frame object across navigations.
content_frame returns None for an iframe that has not finished loading. If you call a method on None, you get an AttributeError. Check the result before use, or switch to frame_locator() which handles the waiting internally.
Multiple pages in the same context share cookies and storage, but each page has its own JavaScript state. If you close a page with page.close(), any locator still pointing at that page fails. Keep references to the pages you still need, and let Playwright's context manager clean up when the browser closes.
The sync API and async API behave the same way for these features. In the async API, expect_page() and expect_popup() are used with async with, and locator calls are awaited. The waiting semantics and frame resolution rules are identical, so the patterns above translate directly.