Track Amazon prices from Python, without the two bugs that silently corrupt almost every price dataset.
pip install git+https://github.com/thunderbit-operations/amazon-price-scraper-python
amzn-price track asins.txt --marketplace de --out prices.csvasin,marketplace,price_amount,price_currency,list_price_amount,captured_at
B0CR7N9MD9,de,24.99,EUR,29.99,2026-07-28T13:19:54Z
This is a focused wrapper around amazon-product-scraper-python, which does the actual fetching and parsing. If you need the full product record — variants, images, sellers, ranks — use that directly. This package exists for the narrower job of collecting prices over time, and for the price-specific traps below.
This is the node every tutorial tells you to read. Here is what Amazon actually serves:
<span class="a-price">
<span class="a-offscreen">₹150.00</span>
<span aria-hidden="true">
<span class="a-price-symbol">₹</span>
<span class="a-price-whole">150<span class="a-price-decimal">.</span></span>
<span class="a-price-fraction">00</span>
</span>
</span>.a-price-whole contains a nested .a-price-decimal, so its text content is
"150." — not "150". Depending on your locale settings that either parses to
the wrong number or throws. Neither outcome is loud.
The correct node is .a-offscreen: the fully formatted, locale-correct string
that screen readers get. This package reads that one, and the underlying library
rejects any selector mentioning .a-price-whole at config load time rather
than documenting the hazard and hoping.
Fetch amazon.co.jp from a US IP address and the prices come back in USD,
not yen. Amazon localises currency to the detected shopper locale — IP geography
plus the delivery-location cookie — independently of the TLD.
Every price scraper surveyed maps TLD → currency. That produces a dataset where
some rows are yen and some are dollars, all labelled JPY, with no way to tell
them apart afterwards. This is not a recoverable error: once the rows are
written, the true currency of any given row is gone.
Here, currency is always parsed from the page and carried alongside the amount:
price.amount # Decimal("3.34")
price.currency # "USD" -- what the page said, not what the TLD implies
price.matches_expected("JPY") # False -> surfaced, not swallowedEvery record is checked against what that storefront would normally quote, and mismatches are reported rather than silently normalised away. To pin a currency, route through an in-country proxy and assert on the detected location.
amzn-price track asins.txt --marketplace de --out prices.csvAppends one row per ASIN. Re-running skips ASINs already captured today unless
you pass --no-resume, so a killed run resumes instead of re-fetching.
amzn-price get B0CR7N9MD9 --marketplace inimport asyncio
from amazon_price_scraper import prices
async def main():
async for row in prices(["B0CR7N9MD9", "B08N5WRWNW"], marketplace="in"):
if row.price is None:
print(f"{row.asin}: {row.status}") # blocked / not_found / no_price
continue
print(f"{row.asin}: {row.price.amount} {row.price.currency}")
asyncio.run(main())Rows are yielded as they complete and carry a status rather than raising, so one blocked page does not abort a run of ten thousand.
| Field | Notes |
|---|---|
price_amount |
Decimal, never a float |
price_currency |
Parsed from the page, ISO 4217 |
list_price_amount |
The struck-through price, when shown |
currency_matches_marketplace |
False means the storefront quoted a foreign currency |
availability / in_stock |
An out-of-stock product often has no price at all |
provenance |
Which extraction strategy produced the price — see below |
status |
ok, not_found, blocked_*, no_price |
Each price records how it was extracted. When Amazon moves the price container, extraction usually keeps working via a weaker fallback — the numbers still arrive and still look fine. The provenance shift is the only early signal you get, and for a time series it matters more than for a one-off scrape: a silent change in which container you are reading can change which price you are collecting (current vs list vs subscribe-and-save).
Amazon runs three anti-bot stacks and all three answer with a 2xx status
code. A scraper that checks resp.status_code records the challenge page as a
product and writes a null price — which in a price history is
indistinguishable from a genuine out-of-stock.
This package inherits the underlying library's classifier, which identifies each
stack by header and signature and asserts positively that the product marker is
present before accepting the page. A blocked row is labelled blocked_*, never
written as a missing price.
It also knows a real 404 from a block, so a delisted ASIN does not get retried against fresh proxies.
Rate limiting is on by default (~1 request/second per IP, jittered) and
robots.txt is respected by default. This package ships no CAPTCHA solving, no
proof-of-work computation and no rate-limit evasion, and pull requests adding
them will be declined.
Amazon's Conditions of Use specifically name "any collection and use of any product listings, descriptions, or prices". Whether such terms bind a client that never logs in is genuinely contested — read the full legal section in the parent project before deploying this. Nothing here is legal advice, and you are responsible for your own use.
If an official API meets your need, prefer it: the Creators API for affiliate data (Product Advertising API 5.0 is deprecated), or SP-API for your own seller account.
This project is not affiliated with, authorised or endorsed by Amazon.
Built and maintained by the team behind Thunderbit, an AI web scraper, alongside the core library this package is a thin layer over.
No account, API key or quota is involved — nothing here calls a Thunderbit service. If you would rather not keep a price tracker working as Amazon changes its markup, Thunderbit is the hosted answer to the same problem.
Apache-2.0 — free for commercial use. See LICENSE.