Selenium Local Storage in Python: Set, Restore, Extract

Replaced broken advanced examples and unsupported performance claims with a runnable catalog experiment in Chrome and Firefox. The code, captures and result definitions distinguish changing storage from changing the data you extract.
Use Selenium Python's execute_script to access localStorage in the current document. Navigate to the intended origin first, pass keys and values as arguments, and include return when you need a value back in Python. If the application reads a preference at startup, set it before entering that application page or trigger its documented refresh afterward.
The last step matters for extraction. In our fixture, writing eu after the table loaded changed localStorage but left all four prices in USD. A wait that checked only the stored key would have accepted the wrong dataset. The examples below check storage, the application's request and the resulting records.
Read, set, remove and inspect localStorage
These helpers are copied from the packet's storage_state.py. They assume driver already points at a document on the intended HTTP(S) origin:
def set_item(driver, key, value):
if not isinstance(key, str) or not isinstance(value, str):
raise ValueError('localStorage helper accepts strings; encode JSON explicitly')
driver.execute_script('localStorage.setItem(arguments[0], arguments[1]);', key, value)
def get_item(driver, key):
return driver.execute_script('return localStorage.getItem(arguments[0]);', key)
def remove_item(driver, key):
driver.execute_script('localStorage.removeItem(arguments[0]);', key)
To inspect all entries, the snapshot helper executes return Object.keys(localStorage).sort().map(k => [k, localStorage.getItem(k)]);. The result is a list of string pairs. The return belongs inside the JavaScript source; without it, there is no returned storage value to use in Python.
LocalStorage stores strings and belongs to an origin. Encode a Python object with json.dumps before passing it to set_item, then decode a nonmissing returned string with json.loads. The packet checks that browser JSON.parse and Python decoding recover the same nested value. Removing one key is preferable when unrelated application preferences should survive; reserve localStorage.clear() for an intentional reset of that origin.

Selenium executes this script in the current window or frame. The storage belongs to that selected document, not to a global WebDriver jar. Our snapshot guard therefore reads location.origin in the script context before changing anything.
Run a complete extraction before adapting it
Use Python 3.12 or newer and installed Chrome/Chromium. Check out the exact evidence snapshot, then enter its example directory:
git clone https://github.com/ScrapingAnt/scrapingant-examples.git
cd scrapingant-examples
git checkout 5dcd4058ba609fd86d7ffaad98deb0d8d2df6714
cd examples/selenium-local-storage
The locked dependency file pins the tested Selenium package and its dependencies:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-lock.txt
./run.sh
The runner starts its own loopback catalog, executes the quickstart, runs three Chrome rounds and validates their captures. It writes ignored run_output/, preserving the committed observations. Selenium Manager can download a driver when needed. The README lists binary/driver overrides and the optional Firefox command. No account or API key is needed; Chrome's sandbox stays enabled by default.
Here is the entire quickstart.py, run with its adjacent packet helpers:
"""Complete bootstrap → safe seed → navigate → wait for actual records example."""
import json
from browser_support import browser, wait_catalog
from catalog_fixture import fixture_server
from oracle import exact_dataset
from storage_state import set_item
def main():
with fixture_server() as (_, origin), browser() as driver:
driver.get(origin+'/blank') # A document on the target origin, before application startup.
set_item(driver, 'demo_region', 'eu')
driver.get(origin+'/catalog')
page = wait_catalog(driver, stored_region='eu', rendered_region='eu')
assert exact_dataset(page['records'])
print(json.dumps({'stored_region': page['stored_region'], 'request_path': page['request_path'],
'records': page['records'], 'matching_records': 4}, indent=2))
if __name__ == '__main__':
main()
The /blank route is a document on the fixture's origin that does not start the catalog application. It lets the script set the preference before /catalog reads it. On a real site, use an appropriate page on the intended origin or the site's actual preference flow; the bootstrap route is part of this fixture, not a path every website provides.
The captured output is:
{
"stored_region": "eu",
"request_path": "/api/catalog?region=eu",
"records": [
{
"currency": "EUR",
"price_minor": 825,
"sku": "SKU-1"
},
{
"currency": "EUR",
"price_minor": 1675,
"sku": "SKU-2"
},
{
"currency": "EUR",
"price_minor": 2425,
"sku": "SKU-3"
},
{
"currency": "EUR",
"price_minor": 3350,
"sku": "SKU-4"
}
],
"matching_records": 4
}
price_minor represents integer minor currency units in this fixture. The four expected SKU/currency/price tuples are literal values in oracle.py, separate from the server's catalog. The comparison rejects missing, extra, duplicate, wrong-currency and wrong-price rows. A separate literal USD dataset validates the wrong-state controls.
What changed the extracted records?
At startup, the fixture chooses its URL's region first, then localStorage.demo_region, then its US default. Its JavaScript explicitly includes the selected value in /api/catalog?region=.... LocalStorage does not attach itself to HTTP requests.
The primary run contains 72 extraction observations: 60 browser-page observations and 12 direct HTTP controls. Each of the following cases ran three times with the Chrome group and three times with the Firefox group. The HTTP controls are repeated alongside those groups; they are not browser-engine measurements.
| Case | Stored preference | Records | Desired EUR matches per observation |
|---|---|---|---|
| Start with an empty profile | Missing | USD | 0/4 |
Seed on /blank, then visit /catalog | eu | EUR | 4/4 |
| Write after the catalog has loaded | eu | Existing USD | 0/4 |
| Reload after that late write | eu | EUR | 4/4 |
| Visit the same host on another port | Missing | USD | 0/4 |
Fresh driver, /catalog?region=eu | Missing | EUR | 4/4 |
| Put the preference only in sessionStorage | Missing from localStorage | USD | 0/4 |
| Wait for the application's delayed update | eu | EUR | 4/4 |
| Fresh driver without restoring state | Missing | USD | 0/4 |
| Restore the exact-origin snapshot | eu | EUR | 4/4 |
| Direct HTTP request with the supported region query | No browser storage | EUR | 4/4 |
| Direct HTTP request without that query | No browser storage | USD | 0/4 |
All 120 scenario assertions passed: the 72 extraction expectations plus 48 separate diagnostics. This includes deliberately wrong-state controls producing their expected USD records. It is not a production success rate. Raw observations and the strict summary preserve the records and request evidence behind each row.
Wait for the application result, not just the key
The asynchronous case starts with a US catalog and a scheduled application update. The application then changes its stored preference, fetches EU data and renders it. The runner first records the incomplete state, so the test does not silently begin after the operation finished.
wait_catalog uses an explicit wait whose predicate requires the intended storage value, the expected rendered region, completion state and the four correct records. A regression test supplies storage-correct but data-stale output and confirms that it does not satisfy the predicate. The runner does not use an arbitrary sleep to declare success.
This is the extraction-specific use of Selenium's explicit waits: choose a condition that means the requested data is ready. A present table or completed navigation is insufficient in this fixture. Adapt the record predicate to your target's identifiers and meaningful values, rather than assuming one currency label proves the whole table is correct.
Restore only the origin you meant to restore
The manual helper exports an origin plus string pairs, validates the snapshot, and restores it in a fresh driver on the same running origin. The test JSON-serializes the snapshot and restores all three synthetic entries; it does not demonstrate browser-profile persistence or save a real authenticated session.
Restoration replaces that origin's localStorage. The helper validates every entry and the selected document's exact origin before clearing anything. A wrong-port restore raises ValueError and leaves the other origin empty. The matrix also shows that merely visiting that second port selects the default US data.
Use this replacement behavior only when you own the intended snapshot and want that reset. For a single preference, the smaller set_item operation avoids clearing unrelated values. Keep a production export private: these helpers make no encryption or credential-protection claim. They do not export cookies, IndexedDB or a complete browser profile.
Errors and controls worth keeping in your scraper
These diagnostic cases and extraction controls each ran six times:
- Opaque origin: reading localStorage from the fixture's
data:document raisedSecurityError. Begin on the intended web origin; disabling browser security was not part of the solution. - Unsafe source interpolation: an apostrophe-containing probe broke the controlled old-style JavaScript string and raised
JavascriptException. Passing the key and value separately preserved apostrophes, newlines and Unicode. - Selective cleanup: removing
demo_regionleft the unrelatedkeep_mevalue intact. - Wrong storage system: setting only sessionStorage did not change this application, which reads localStorage. That is an application contract, not evidence that the two stores are interchangeable.
The packet also runs 22 regression tests, separately from the browser matrix. They cover origin and snapshot validation, record correctness, incomplete or contradictory captures, partial failures and local requests that ignore ambient proxy settings. Earlier exploratory captures remain available and are excluded from the final counts.
When ScrapingAnt can handle the retrieval step
The fresh-driver URL case offers a practical exit from a custom state workflow: /catalog?region=eu rendered the desired records while localStorage stayed empty. The direct query control also retrieved the desired JSON without a browser. Both work because this application explicitly provides that selection mechanism. Check whether your target already offers an equivalent URL or supported cookie contract before building a snapshot pipeline.
With browser=true, ScrapingAnt renders the target page with JavaScript and returns its HTML. With browser=true, wait_for_selector tells ScrapingAnt to wait for the specified DOM element to appear. When the desired view is already addressable by URL, you can use ScrapingAnt's browser rendering for HTML retrieval and keep your record validation after the response. A selector alone does not establish the right account, currency or dataset.
ScrapingAnt does not support localStorage operations. Keep browser automation when your task requires setting, exporting or restoring that state. For targets that actually use cookies instead, ScrapingAnt accepts cookies as name/value pairs through the cookies parameter, with multiple pairs separated by semicolons. That is a separate supported input, not a conversion of a localStorage snapshot.
A request with JavaScript rendering through a datacenter proxy costs 10 API credits. This is the documented cost; the local experiment made no ScrapingAnt requests. ScrapingAnt is unnecessary for this local fixture or a direct retrieval you already operate. Its relevant role is retrieving rendered HTML after the target's required view can be expressed without localStorage control.
Reproduce the second browser and respect the scope
With Firefox installed, run:
./run.sh --all-browsers
These are three repeats of a deterministic catalog on one host. They establish the tested state-to-data behavior, not a speed advantage, production reliability estimate or login portability. Persistent profiles, browser restarts, frames, real accounts, quotas and encryption were not browser experiments here. The selected-document origin guard has a helper regression; it is not evidence of a frame-based browser run.
For related tasks, see Selenium cookies, Playwright localStorage and Puppeteer localStorage.
Examples tested on 2026-09-27 with Python 3.12.10, Selenium 4.49.0, Chrome/ChromeDriver 154.0.8037.57, Firefox 156.0 and GeckoDriver 0.37.1 on macOS 26.6.2 arm64. Code and evidence.
This article was drafted with AI assistance from a tested evidence packet. Oleg Kulyk is responsible for the article, measurements and corrections.