Skip to content

05 · File Upload & Download

Uploading via set_input_files

Most upload widgets are, underneath any custom styling, an <input type="file">. Playwright can set its value directly — no need to interact with the OS-native file picker dialog at all, which is the right approach: OS dialogs aren't part of the DOM and can't be automated reliably or portably across CI environments.

from playwright.sync_api import Page

def test_upload_resume(page: Page):
    page.goto("https://example.com/apply")
    page.locator("input[type='file']").set_input_files("fixtures/resume.pdf")
    page.get_by_role("button", name="Submit application").click()

    from playwright.sync_api import expect
    expect(page.get_by_text("resume.pdf uploaded")).to_be_visible()
1 passed in 1.20s

set_input_files works even if the input is visually hidden by CSS (display: none or opacity: 0, a common technique so a styled button triggers a native input under the hood) — Playwright can set files on it directly without needing it to be visible, since no real click on the OS dialog is involved.

Multiple files, and clearing a selection

page.locator("input[type='file']").set_input_files(
    ["fixtures/photo1.jpg", "fixtures/photo2.jpg"]
)
page.locator("input[type='file']").set_input_files([])  # clears the selection
# no output for either — a list of paths uploads multiple files
# at once (for inputs with the `multiple` attribute); an empty
# list clears whatever was previously selected

Uploading a file created in-memory

Sometimes there's no file on disk to reference — you want to test a CSV upload with content generated by the test itself.

page.locator("input[type='file']").set_input_files(
    files=[{
        "name": "data.csv",
        "mimeType": "text/csv",
        "buffer": b"name,age\nAda,30\nGrace,85\n",
    }]
)
# no output — Playwright writes this in-memory buffer as the
# uploaded file's content, with no temp file ever touching disk

Triggering upload via a file chooser event

Some UIs open the file input only after a button click dispatches a programmatic .click() on a hidden input — in that case, wrap the triggering click in expect_file_chooser.

with page.expect_file_chooser() as fc_info:
    page.get_by_role("button", name="Choose file").click()
file_chooser = fc_info.value
file_chooser.set_files("fixtures/resume.pdf")
# no output — expect_file_chooser listens for the native file
# dialog opening (fired as a page event even though the real OS
# dialog never actually appears in headless mode) and captures it

Downloading files

A download starts when the page navigates to a downloadable resource or a link/button triggers one. Playwright captures it as a Download object via expect_download, without your test needing to interact with the browser's actual download manager UI.

def test_export_report_downloads_csv(page: Page):
    page.goto("https://example.com/reports")
    with page.expect_download() as download_info:
        page.get_by_role("button", name="Export as CSV").click()
    download = download_info.value

    assert download.suggested_filename == "report.csv"
    download.save_as(f"downloads/{download.suggested_filename}")
1 passed in 1.44s

download.suggested_filename reflects the filename the server/browser would use by default (from Content-Disposition or the URL); save_as writes the downloaded bytes to a path you control, which you can then open and assert on the actual content.

Asserting on downloaded content

import csv

def test_exported_csv_has_expected_rows(page: Page):
    page.goto("https://example.com/reports")
    with page.expect_download() as download_info:
        page.get_by_role("button", name="Export as CSV").click()
    download = download_info.value

    path = download.path()  # temp path Playwright already saved it to
    with open(path, newline="") as f:
        rows = list(csv.reader(f))

    assert rows[0] == ["Date", "Revenue", "Orders"]
    assert len(rows) > 1
1 passed in 1.51s

download.path() gives you the temporary file Playwright already wrote the download to internally, without needing save_as at all when you only need to inspect content rather than keep a permanent copy — handy for assertions that don't need to persist the artifact past the test.

Configuring the download directory for a whole run

# conftest.py
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {**browser_context_args, "accept_downloads": True}
# no output — accept_downloads defaults to True in modern
# Playwright versions, but making it explicit documents intent
# and matters if a project has changed the default

How It Actually Works

set_input_files doesn't simulate mouse clicks and OS file-picker dialogs at all — Playwright talks to the browser's DevTools Protocol (CDP) directly and calls DOM.setFileInputFiles, which writes the file paths straight into the <input type="file"> element's internal file list, bypassing the OS dialog entirely (that's why it works headlessly, where no real dialog could even render). Downloads work the same way in reverse: Playwright intercepts the browser's Page.downloadWillBegin/downloadWillFinish CDP events, which fire the instant the browser's network layer decides a response is a download (based on Content-Disposition or MIME type) — the download object you get in your script is just a handle to that in-flight browser-side download, which is why you still need to explicitly save_as() it to control where the file actually lands on disk.

Exercise

Using https://the-internet.herokuapp.com/upload (upload) and https://the-internet.herokuapp.com/download (a page listing downloadable files):

  1. Create a small local text file fixture, upload it via input[type='file'], click "Upload", and assert the resulting page shows the correct filename in #uploaded-files.
  2. Upload an in-memory buffer (no file on disk) with set_input_files using the files=[{...}] form, and confirm it uploads successfully.
  3. On the download page, pick any linked file, wrap the click in expect_download(), and assert download.suggested_filename matches the link text.
  4. Save the downloaded file with save_as() into a downloads/ folder in your project, then open it with plain Python and assert it's non-empty.
  5. Write a test that uploads a file whose size exceeds a limit your app enforces (pick any oversized fixture you create), and assert the correct client-side validation error appears instead of a successful upload.