Resource library

QA How-To

Selenium handling file download in Python (2026)

Learn selenium handling file download python with pytest code, filesystem waits, format validation, requests streaming, Grid retrieval, and CI practices.

19 min read | 2,858 words

TL;DR

Use a unique temporary directory, configure it before WebDriver starts, and poll until the intended final file exists with no partial markers. Parse the result, or use requests for payload tests and enabled RemoteWebDriver methods for Grid retrieval.

Key Takeaways

  • Configure a unique pytest tmp_path in browser preferences before creating the driver.
  • Wait for the exact final file and reject .crdownload and .part markers instead of sleeping.
  • Parse the promised format and assert business data because extensions can hide error responses.
  • Use requests with carefully transferred browser cookies for payload-focused coverage.
  • Enable downloads and retrieve node-side files through RemoteWebDriver when running on Grid.
  • Keep browser setup, UI triggers, completion observation, validation, and cleanup separate.
  • Protect downloaded reports as potentially sensitive CI artifacts.

For selenium handling file download python, create an isolated temporary directory, configure the browser to save there without prompting, click the user-facing control, and use WebDriverWait to poll the filesystem until the final file exists and partial download markers disappear. Finish by parsing the file and asserting business content.

That browser pattern proves the real download journey. When the requirement is primarily the HTTP response or report data, transfer the authenticated browser cookies into requests and download through an HTTP session. When the browser runs on Selenium Grid, enable remote downloads and retrieve the node's file through RemoteWebDriver. This guide gives you runnable pytest code for each decision.

TL;DR

def completed_file(directory: Path, name: str):
    target = directory / name
    partials = list(directory.glob("*.crdownload"))
    partials += list(directory.glob("*.part"))
    if target.is_file() and target.stat().st_size > 0 and not partials:
        return target
    return False


downloaded = WebDriverWait(driver, 30, poll_frequency=0.2).until(
    lambda _: completed_file(download_dir, expected_name)
)

A page toast or successful click is not proof that bytes reached disk. Use one empty directory per test, wait for the exact artifact, reject partial files, validate the promised format, and clean up safely.

1. Scope selenium handling file download python Correctly

Download tests become flaky when they combine too many vague expectations. Separate the behavior into four stages: the UI exposes the export, the browser initiates the request, the transfer completes, and the file content satisfies the product contract. Each stage deserves a distinct failure signal.

Ask what the test must prove. If a user must click Export and receive a file with a certain name, a real browser download is appropriate. If dozens of role and filter combinations must produce correct CSV data, an authenticated requests test is more efficient. If a remote browser is mandatory, the file initially exists on the remote node, not beside the Python process.

A robust oracle might require exactly one new file, a filename matching the requested report, no .crdownload or .part marker, nonzero size, a valid format signature, expected column headers, and a known record. Avoid assertions such as "the directory is not empty." Screenshots, logs, or leftovers can satisfy them.

The DOM does not expose transfer completion. An enabled button proves only that an action can be attempted. A success toast may represent server job creation rather than file delivery. Selenium's ordinary local API intentionally focuses on user interaction and does not offer a universal wait_for_download method.

Design the test name to communicate its layer. test_user_can_download_invoice_pdf promises browser behavior. test_invoice_export_contains_tax_columns promises payload semantics and may be better with an HTTP client. Clear names prevent accidental coverage claims.

For reliable interaction before the transfer begins, consult the Selenium waits in Python guide.

2. Configure Chrome with pathlib and pytest tmp_path

pytest's tmp_path fixture provides a unique Path for each test invocation. Pass its absolute string to Chrome preferences before driver creation. This avoids manual directory naming and works naturally with parallel pytest workers.

import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


@pytest.fixture
def download_browser(tmp_path):
    options = Options()
    options.add_experimental_option(
        "prefs",
        {
            "download.default_directory": str(tmp_path.resolve()),
            "download.prompt_for_download": False,
            "download.directory_upgrade": True,
        },
    )

    browser = webdriver.Chrome(options=options)
    yield browser, tmp_path
    browser.quit()

Keep browser preferences inside the driver factory or a focused fixture. Feature tests should not repeat browser-specific configuration. The directory must exist and be writable by the browser process. tmp_path meets those needs locally, but a container still needs suitable filesystem permissions.

Do not modify the preference after starting Chrome. The browser profile already exists. Do not use a relative path, because the browser and Python process may resolve it differently. Avoid a permanent Downloads folder because old files and automatic duplicate suffixes corrupt the oracle.

Headless mode should be added through the supported browser argument in your environment and validated in CI. Do not copy obsolete versioned DevTools download commands simply because an old article says headless Chrome needs them. Keep any browser-specific fallback isolated and covered by a compatibility test.

pytest deletes temporary areas according to its own retention behavior, but explicit artifact policy still matters. Sensitive reports should not be uploaded automatically. On failure, preserve only what the team has approved, or log sanitized metadata such as name, size, and digest.

3. Runnable pytest File Download Test

The following complete test selects a text download on a public demonstration page. It waits for the exact final filename and rejects Chrome or Firefox partial extensions. The example is intentionally format-light, so adapt the final assertions to your application's report.

from pathlib import Path

import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait


def completed_file(directory: Path, file_name: str):
    target = directory / file_name
    partials = list(directory.glob("*.crdownload"))
    partials.extend(directory.glob("*.part"))

    if target.is_file() and target.stat().st_size > 0 and not partials:
        return target
    return False


@pytest.fixture
def download_browser(tmp_path):
    options = Options()
    options.add_experimental_option(
        "prefs",
        {
            "download.default_directory": str(tmp_path.resolve()),
            "download.prompt_for_download": False,
            "download.directory_upgrade": True,
        },
    )
    browser = webdriver.Chrome(options=options)
    yield browser, tmp_path
    browser.quit()


def test_downloads_complete_text_file(download_browser):
    driver, download_dir = download_browser
    driver.get("https://the-internet.herokuapp.com/download")

    links = driver.find_elements(By.CSS_SELECTOR, "a[href]")
    link = next(item for item in links if item.text.endswith(".txt"))
    expected_name = link.text

    link.click()

    downloaded = WebDriverWait(
        driver, 30, poll_frequency=0.2
    ).until(
        lambda _: completed_file(download_dir, expected_name),
        message=f"Download did not complete: {expected_name}",
    )

    assert downloaded.stat().st_size > 0
    assert downloaded.read_text(encoding="utf-8").strip()

Run it with python -m pytest -q. Public demo files can change and should not be a release gate. A controlled fixture endpoint produces more stable names and content.

Returning the Path from the wait condition is useful. WebDriverWait returns the successful non-false value, so the test can immediately validate the exact artifact without rescanning the directory.

4. Build a Better Completion Observer

The simple helper is correct for deterministic filenames in an isolated directory. Real products may generate report-2026-07-13.csv, redirect to signed storage, or stream large files. Extend the observer based on an explicit contract, not with a global "pick newest file" shortcut.

Take a set of paths before the click and compare it with each later directory snapshot. Filter new regular files, reject partial suffixes, and apply a strict filename pattern. If exactly one candidate remains, test its size. This approach works when the final name is generated but prevents unrelated existing files from winning.

A large transfer can benefit from a stable-size rule. Record the candidate's size, wait another poll, and require it to remain unchanged after partial markers disappear. Be careful: stable size alone can describe a stalled transfer. Combine signals and validate format afterward.

Use pathlib for all path work. If a server-provided Content-Disposition name influences the local target, strip path components and confirm the normalized path stays under the intended directory. A malicious ../../ path must never escape the test sandbox.

Timeout diagnostics should report sanitized directory entries and sizes, not entire file contents. Distinguish "no request started," "partial marker remained," "multiple candidates appeared," and "final file failed validation." Those messages lead engineers to different owners.

Never wait forever. Set a product-relevant maximum and fail when it is exceeded. A generated report that routinely needs longer may be an asynchronous job workflow. In that case, test job status through its API and reserve one browser case for the final user flow.

The Selenium flaky test troubleshooting guide provides complementary techniques for timing and state isolation.

5. Selenium handling file download python Across Browsers

Chrome and Firefox use different preferences and temporary extensions. A cross-browser suite must configure and observe each supported browser rather than assuming Chrome settings apply everywhere.

For Firefox, configure the download folder and the MIME types the application actually returns:

from selenium import webdriver
from selenium.webdriver.firefox.options import Options

options = Options()
options.set_preference("browser.download.folderList", 2)
options.set_preference("browser.download.dir", str(download_dir.resolve()))
options.set_preference("browser.download.useDownloadDir", True)
options.set_preference("browser.download.manager.showWhenStarting", False)
options.set_preference(
    "browser.helperApps.neverAsk.saveToDisk",
    "text/csv,application/pdf,application/octet-stream",
)

driver = webdriver.Firefox(options=options)

Do not add an unlimited MIME list merely to suppress every dialog. If the server labels a CSV as text/html because it returned an error page, the test should expose that defect. Verify Content-Type at the protocol layer and parse the downloaded artifact.

PDFs can open in an internal browser viewer rather than save. Decide which behavior the product requires and set the browser profile accordingly. A preference that forces every PDF to disk changes browser behavior, so document that it is test configuration.

Browser context Common partial marker Configuration concern
Chrome or Edge .crdownload Download preferences must exist at session creation
Firefox .part MIME handling and built-in viewers affect saving
Safari Browser-managed behavior Automation and download support need explicit proof
Remote Grid Node-side storage Local paths do not point into the node
Managed enterprise browser Policy-dependent Organizational policies can override preferences

Use a small contract test per browser. Keep browser-specific code in fixtures, while content validators remain browser-neutral.

6. Validate CSV, PDF, ZIP, and XLSX Content

A complete file can still be wrong. Parse it using the promised format and assert business facts. Filename extensions are hints, not proof.

Python's csv module handles standard CSV quoting and embedded delimiters when used correctly:

import csv

with downloaded.open(newline="", encoding="utf-8-sig") as stream:
    rows = list(csv.DictReader(stream))

assert rows
assert {"employee_id", "status", "total"}.issubset(rows[0])
assert any(row["status"] == "Active" for row in rows)

For large exports, iterate instead of converting every row to a list. Check the header first, count rows incrementally, and retain only the records needed for assertions.

For PDF, confirm the file begins with b"%PDF-" and use a maintained parser when text or metadata assertions matter. For ZIP, use zipfile.ZipFile.testzip and inspect entry names. Before extraction, reject absolute paths and parent traversal segments. For XLSX, use an approved spreadsheet library such as openpyxl in read-only mode for large workbooks.

Check content before uploading an artifact. An HTML error page often starts with markup and can explain why a supposed CSV parser failed. Record only a safe prefix when policy allows, because exports may contain personal or financial information.

Hashes work well for immutable binary fixtures. They are weak for reports containing timestamps, generated identifiers, workbook metadata, or nondeterministic ordering. Semantic assertions better express user expectations in those cases.

Also validate freshness. A unique tmp_path largely solves stale artifacts. If the report embeds a request ID, date, filter, or account, assert it. That proves the file belongs to this action rather than an earlier cached response.

7. Use requests for Payload-Focused Tests

When real browser saving is not the requirement, let Selenium establish the authenticated application state and use requests for the file transfer. This provides direct status and headers, supports streaming, and avoids browser-specific temporary-file behavior.

from pathlib import Path

import requests
from selenium.webdriver.common.by import By

download_url = driver.find_element(
    By.CSS_SELECTOR, "[data-testid='export']"
).get_attribute("href")

session = requests.Session()
for cookie in driver.get_cookies():
    session.cookies.set(
        cookie["name"],
        cookie["value"],
        domain=cookie.get("domain"),
        path=cookie.get("path", "/"),
    )

target = Path("build/downloads/report.csv")
target.parent.mkdir(parents=True, exist_ok=True)

with session.get(download_url, stream=True, timeout=(5, 60)) as response:
    response.raise_for_status()
    assert "text/csv" in response.headers.get("Content-Type", "")
    with target.open("wb") as output:
        for chunk in response.iter_content(chunk_size=64 * 1024):
            if chunk:
                output.write(chunk)

assert target.stat().st_size > 0

Install requests as a declared test dependency. Filter cookies and requests to the intended HTTPS host. Never print the Cookie header. Some applications also require CSRF tokens, bearer tokens, signed query parameters, or a particular request method, so reproduce the documented endpoint contract rather than guessing.

This test does not prove browser save behavior. Name it as an export API or payload test. Keep one browser contract if users depend on the click and downloaded filename, then cover content permutations through requests.

Streaming avoids loading the whole response into memory. Set separate connection and read timeouts, reject unexpected redirects when security requires it, validate Content-Disposition safely, and delete or protect the artifact after parsing.

8. Remote Grid Downloads in Python

A browser launched by webdriver.Remote writes to its node's filesystem. The Python process cannot watch that location through tmp_path. Enable Selenium's remote download capability, trigger the UI action, wait until the remote session lists the file, and ask RemoteWebDriver to retrieve it.

from pathlib import Path

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait

options = Options()
options.enable_downloads = True

driver = webdriver.Remote(
    command_executor="http://localhost:4444",
    options=options,
)
try:
    driver.get("https://app.example.test/reports")
    driver.find_element("css selector", "[data-testid='export']").click()

    names = WebDriverWait(driver, 30).until(
        lambda current: current.get_downloadable_files() or False
    )
    report_name = next(name for name in names if name.endswith(".csv"))

    local_dir = Path("build/grid-downloads").resolve()
    local_dir.mkdir(parents=True, exist_ok=True)
    driver.download_file(report_name, str(local_dir))
    driver.delete_downloadable_files()
finally:
    driver.quit()

Use the real Grid URL and application selectors. The current Python binding exposes get_downloadable_files, download_file, and delete_downloadable_files on RemoteWebDriver. options.enable_downloads requests the se:downloadsEnabled capability.

A nonempty remote list is not enough if the session can download several files. Snapshot names before the click or start a fresh session, then select the exact expected result. After retrieval, run the same local parser validations used in non-Grid tests.

Cloud providers may implement or restrict the capability differently. Confirm their supported Selenium and browser versions. Do not assume a locally mounted path maps into a hosted browser. The Selenium Grid file handling guide covers remote-session design in more depth.

9. Parallel pytest, xdist, and Artifact Safety

pytest-xdist runs tests in separate worker processes, but a hard-coded Downloads directory defeats that isolation. tmp_path is worker-safe and test-specific. If a session-scoped browser is unavoidable, allocate a unique directory per session and serialize downloads that share it, though function-scoped isolation is usually clearer.

Avoid tests that find max(directory.iterdir(), key=mtime). Modification time resolution, clock behavior, concurrent writes, and background files make "newest" unreliable. Use an exact name or a before-and-after snapshot with strict filtering.

Quit the browser before deleting its directory. Otherwise, the browser can still hold a file handle or finish a delayed write. pytest fixture teardown naturally supports that order. If cleanup fails on a platform due to a transient scanner lock, implement a bounded infrastructure retry and surface persistent failure.

Artifacts can contain secrets or regulated data. Decide whether reports may be retained, who can read them, and how long CI keeps them. A safe failure attachment may contain only the filename, size, SHA-256 digest, parser exception, and a screenshot of the UI. Do not automatically publish raw customer exports.

Monitor disk space in container and Grid nodes. Parallel large exports can exhaust ephemeral storage even when each test eventually cleans up. Use quotas, suite tags, and realistic fixture sizes. Call delete_downloadable_files for remote sessions after successful retrieval and in cleanup when failure permits it.

Keep generated files out of version control. Use the test runner's temporary area or a build directory already excluded by repository rules.

10. A Maintainable Python Download Abstraction

A useful design has small components with single responsibilities:

  • A browser fixture configures a unique local directory or remote capability.
  • A page object triggers the exact user action.
  • A download observer waits for one intended completed artifact.
  • A format validator parses CSV, PDF, ZIP, or XLSX.
  • A lifecycle layer cleans and optionally preserves approved failure evidence.

Do not put all five responsibilities into download_report. When it times out, no one knows whether the button locator, transfer, filename, parser, or cleanup failed.

Model the observer as a function that accepts Path, a filename or predicate, partial suffixes, and timeout policy. Return Path on success. Unit test it by creating a partial file, renaming it, writing content, and creating distracting files in a temporary directory. These tests run without a browser and make edge cases cheap to cover.

Keep the remote adapter separate because its signals are remote API names, not local directory entries. Both adapters can return a local Path after completion, allowing the same validators to operate downstream.

Page objects should not parse files. Validators should not know Selenium. This clean boundary also supports requests-based payload tests. A report validator can receive a Path whether the bytes arrived through Chrome, Firefox, Grid, or requests.

Prefer explicit configuration over environment guessing. The driver factory should know whether execution is local or remote. The test should know the expected report. A helper should never silently fall back from a failed browser download to requests, because that would change the behavior being proved.

Interview Questions and Answers

Q: How do you wait for a file download in Selenium Python?

Poll the filesystem with a bounded wait until the exact final file exists, has a valid size, and no browser partial marker remains. WebDriverWait can return the successful Path. Do not use time.sleep as the completion oracle.

Q: Why is tmp_path useful for download tests?

It gives each pytest case an isolated Path, preventing stale files, filename suffix collisions, and parallel-worker interference. It also simplifies cleanup and diagnostics.

Q: What is the difference between a browser download test and a requests test?

The browser test proves user interaction and browser save behavior. The requests test proves the endpoint response and payload using HTTP semantics. Both are valuable, but they make different coverage claims.

Q: How do you retrieve a download from Selenium Grid?

Set options.enable_downloads before creating webdriver.Remote, trigger the file, wait for get_downloadable_files, and call download_file with the chosen name and local directory. Delete remote files afterward.

Q: Why should a downloaded CSV be parsed?

An empty file, truncated file, or HTML error page can still have a .csv name. Parsing confirms the grammar, and business assertions confirm the report is useful.

Q: How do Chrome and Firefox completion checks differ?

Chrome commonly uses .crdownload during transfer, while Firefox commonly uses .part. Browser preferences and PDF handling also differ, so keep configuration browser-specific and validation common.

Q: What makes a download test safe for parallel execution?

Use a unique directory and driver per case, deterministic file identification, no global newest-file search, and controlled cleanup. Remote sessions also need isolated names or fresh download stores.

Q: How would you debug a download timeout?

I would distinguish whether the click happened, a partial file appeared, a final file appeared under another name, or parsing failed. I would capture sanitized directory metadata, browser and Grid versions, response facts, and the exact stage without exposing report content.

Common Mistakes

  • Calling time.sleep and assuming the transfer finished.
  • Waiting for any file instead of the exact new artifact.
  • Sharing a Downloads directory across pytest workers.
  • Forgetting that Chrome and Firefox use different partial suffixes.
  • Setting browser preferences after WebDriver creation.
  • Using a relative or unwritable directory in a container.
  • Treating a toast as proof that the file reached disk.
  • Trusting an extension without parsing the actual format.
  • Loading a huge response fully into memory instead of streaming.
  • Copying cookies to an unintended host or printing them in logs.
  • Saying a requests test proves browser download behavior.
  • Watching tmp_path for a file written on a remote Grid node.
  • Forgetting options.enable_downloads for RemoteWebDriver.
  • Selecting the first remote filename without isolating the session.
  • Retaining sensitive reports as public CI artifacts.
  • Hiding a browser failure with an automatic requests fallback.

Conclusion

For selenium handling file download python, configure an isolated path before browser startup, trigger the UI, poll for the exact completed artifact, and parse meaningful content. Use requests for response-heavy coverage and RemoteWebDriver's enabled download methods for Grid.

Build one trustworthy browser contract first, then reuse browser-neutral validators across local, remote, and HTTP-client paths. The result is faster diagnosis, safer parallel execution, and download coverage that proves more than a click.

Interview Questions and Answers

Why should Selenium download tests avoid time.sleep?

A fixed delay has no relationship to actual transfer completion. It can be too short in CI and unnecessarily long locally. Poll observable file state with an explicit timeout.

What does a reliable local completion condition check?

It checks the intended final path, nonzero or expected size, and absence of browser partial files. Format parsing then confirms the file is complete and correct.

How does pytest tmp_path improve download automation?

It isolates each case and worker, removing stale-file and collision problems. pathlib operations and fixture teardown make the lifecycle straightforward.

When would you copy Selenium cookies into requests?

I would do it when Selenium establishes the authenticated user context but deep payload verification is better performed over HTTP. I would restrict the request to the intended HTTPS host and never log cookies.

How do remote Selenium downloads differ from local downloads?

The initial file lives on the Grid node rather than the runner's filesystem. The session must enable downloads, and RemoteWebDriver methods list, retrieve, and delete node-side artifacts.

How do you validate a CSV without creating memory problems?

Open it with newline handling and the csv module, verify headers, and iterate rows while tracking only required counts or records. Avoid loading very large exports into a list.

How do you make download tests parallel-safe?

Use one directory and driver per case, compare against a before snapshot or exact name, and never choose the newest file in a shared folder. Isolate remote download stores as well.

What responsibilities should a download helper have?

It should observe completion and return a local Path with good diagnostics. Browser configuration, UI interaction, format validation, and artifact policy should remain separate components.

Frequently Asked Questions

How do I wait for a Selenium download in Python?

Use WebDriverWait with a function that checks the exact final Path, nonzero size, and absence of .crdownload or .part files. Return the Path when complete and False while waiting.

Can pytest detect when a Chrome download is complete?

Yes, pytest can provide an isolated tmp_path and your test can poll it for the final file. The browser does not send ordinary DOM completion state for local downloads.

How do I set the download folder in Selenium Python?

Add an absolute directory to Chrome download preferences or the equivalent Firefox preferences before constructing WebDriver. Use a unique writable directory for each test.

How can I download a Grid file to my Python test runner?

Set options.enable_downloads to True, create webdriver.Remote, trigger the download, call get_downloadable_files, then retrieve the selected name with download_file. Clean the remote store afterward.

Should Selenium or requests test a CSV export?

Use Selenium for the real user click and browser save contract. Use requests for broad status, header, filter, role, and content combinations where browser mechanics are not the subject.

How do I validate that a downloaded PDF is real?

Check the PDF signature and parse the file with a maintained PDF library for required text or metadata. A .pdf extension alone cannot rule out an HTML error response.

Are download files safe to upload as CI artifacts?

Not automatically. Reports may contain personal or confidential data, so define access, retention, redaction, and failure-preservation policies before uploading them.

Related Guides