Yahoo Finance is one of the most widely used public sources for market data. Every quote page carries the same headline numbers a trader watches all day: the current price for a ticker, the absolute change since the previous close, and the change as a percentage. For anyone tracking a watchlist or logging end-of-day prices, copying that by hand for more than a symbol or two gets old fast.

This is a short companion to the broader scrape Yahoo Finance walkthrough. Here you build one small, runnable Python script that takes a ticker symbol and returns its current price, change, and change percent, scoped to public market data anyone can see without an account. For the full multi-field guide, read the sibling post; this one is the quick price-fetch script.

What you will build

A single Python function you call with a ticker symbol. It fetches the rendered Yahoo Finance quote page through the Crawling API, parses three fields, and returns them as a small dictionary. The running example is Apple (AAPL):

  • Price the current quoted price for the symbol.
  • Change the absolute price change since the previous close.
  • Change percent that same change expressed as a percentage.

Why a plain request fails on Yahoo Finance

Point a bare HTTP client at a Yahoo Finance quote URL and you usually get status 200 and a page that does not contain the live numbers you wanted. Two things work against you. First, the quote page renders its price block in the browser through JavaScript, and the figure ticks in real time, so the initial HTML is a shell that fills in only after the page's scripts run. Parse that first response and the price field comes back empty. Second, financial sites watch for automated traffic: datacenter IPs and request patterns that do not look like a real browser get rate-limited or challenged before they reach the rendered content.

So a working price fetcher needs two things in one request: a browser that renders the quote block, and an IP the site reads as a real visitor. You can build that with a headless browser plus rotating residential proxies, but keeping that stack healthy is most of the effort. The Crawling API folds both into a single call: send it the URL with a JavaScript token, it renders the page behind a trusted IP, and returns finished HTML for you to parse.

Why the JS token

Crawlbase offers two token types. The normal token fetches static HTML; the JavaScript (JS) token renders the page in a real browser first. Yahoo Finance fills its price block client-side, so you need the JS token here. The normal token returns the same shell a plain fetch would, with the live numbers missing.

Prerequisites

Three things need to be in place before writing any code.

Basic Python. You should be comfortable running a script and installing packages with pip. If you are new to the parsing side, the BeautifulSoup guide pairs well with this tutorial.

Python 3.8 or later. Confirm your version with python --version. If you do not have it, install it from python.org and make sure Python is on your PATH.

A Crawlbase account and JS token. Sign up, open your dashboard, and copy your JavaScript (JS) token from the account docs page. Crawlbase includes 1,000 free requests to start, more than enough for this script. Treat the token like a password and keep it out of version control.

Set up the project

Create a virtual environment so project dependencies stay isolated, then install the two libraries the script needs.

bash
python --version

python -m venv yahoo_env
source yahoo_env/bin/activate

pip install crawlbase beautifulsoup4

On Windows, activate the environment with yahoo_env\Scripts\activate instead of the source line. Two dependencies do the work: crawlbase is the official client for the Crawling API, and beautifulsoup4 parses the returned HTML so you can pull fields out by attribute. The json module ships with the standard library.

Step 1: Fetch a rendered quote page

Start by getting a finished page. Import the CrawlingAPI class, initialize it with your JS token, and request a quote URL. The quote page is built from a symbol, so the pattern is https://finance.yahoo.com/quote/<SYMBOL>. Pass ajax_wait and page_wait so the price block has loaded before the page is captured, and check the Crawlbase pc_status before parsing so failures stay loud.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

OPTIONS = {
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0",
    "ajax_wait": "true",
    "page_wait": 4000,
}

def crawl(symbol):
    quote_url = f"https://finance.yahoo.com/quote/{symbol}"
    response = api.get(quote_url, OPTIONS)
    if response["headers"]["pc_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['headers']['pc_status']}")
    return None

if __name__ == "__main__":
    html = crawl("AAPL")
    print(html[:500] if html else "No HTML returned")

The wait options matter for a client-rendered target. ajax_wait waits for asynchronous content to load, and page_wait holds for a fixed number of milliseconds after load so the price block is populated before capture. Four seconds is a reasonable start; raise it if the price comes back empty. Run the script with python yahoo_price.py and you should see real Yahoo Finance markup, not the shell a plain request returns, which confirms rendering works before you write a single selector.

Crawlbase Yahoo Finance Scraper

The quote page needs a rendered price block behind a trusted IP, in one call, which is exactly what the ajax_wait and page_wait options above set up. The Crawling API takes a JS token, runs the page in a real browser, rotates through residential IPs server-side, and hands you finished HTML, so you skip running a headless fleet and a proxy pool yourself. Point it at a public quote page on the free tier first.

Step 2: Parse the price, change, and change percent

Yahoo Finance tags its headline quote numbers with stable data-field attributes on the price block, which makes them reliable to target: price sits on regularMarketPrice, the absolute change on regularMarketChange, and the percentage on regularMarketChangePercent. Load the rendered HTML into BeautifulSoup and read each one. Every lookup is guarded so a missing field returns None instead of crashing the script.

python
from bs4 import BeautifulSoup

def field(soup, name):
    el = soup.select_one(f'[data-field="{name}"]')
    return el.get_text(strip=True) if el else None

def parse_quote(html, symbol):
    soup = BeautifulSoup(html, "html.parser")
    return {
        "symbol": symbol,
        "price": field(soup, "regularMarketPrice"),
        "change": field(soup, "regularMarketChange"),
        "change_percent": field(soup, "regularMarketChangePercent"),
    }

The field helper queries one element by its data-field attribute and returns the stripped text, or None when the element is absent, so a symbol whose page omits a value does not break the run. parse_quote bundles the three numbers plus the symbol into a dictionary. Reading attribute-tagged fields is sturdier than relying on Yahoo's generated CSS class names.

Selectors drift

Yahoo Finance's layout and class names change over time, and the data-field attributes can move too. Treat the selectors here as a starting template, not a contract. If a field comes back None, open the live quote page in your browser's dev tools, find the element holding the number, and update the selector.

Step 3: Assemble the full script

Now wire the two pieces into one runnable script. It fetches the rendered quote page, parses the three fields, prints them, and writes the record to a JSON file so the result is easy to pass downstream.

python
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

OPTIONS = {
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0",
    "ajax_wait": "true",
    "page_wait": 4000,
}

def crawl(symbol):
    quote_url = f"https://finance.yahoo.com/quote/{symbol}"
    response = api.get(quote_url, OPTIONS)
    if response["headers"]["pc_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['headers']['pc_status']}")
    return None

def field(soup, name):
    el = soup.select_one(f'[data-field="{name}"]')
    return el.get_text(strip=True) if el else None

def parse_quote(html, symbol):
    soup = BeautifulSoup(html, "html.parser")
    return {
        "symbol": symbol,
        "price": field(soup, "regularMarketPrice"),
        "change": field(soup, "regularMarketChange"),
        "change_percent": field(soup, "regularMarketChangePercent"),
    }

def get_stock_price(symbol):
    html = crawl(symbol)
    if not html:
        return None
    return parse_quote(html, symbol)

def main():
    quote = get_stock_price("AAPL")
    if not quote:
        print("Could not fetch quote")
        return
    print(json.dumps(quote, indent=2))
    with open("quote.json", "w") as f:
        json.dump(quote, f, indent=2)

if __name__ == "__main__":
    main()

The get_stock_price function is the one you call: pass it a ticker symbol and it returns the dictionary, or None when the fetch fails. main runs it for AAPL, prints the result, and saves it to quote.json.

What the output looks like

Run the script and you get a clean structured record, ready for a watchlist, a notebook, or a database. The numbers below are illustrative; your run returns the live values.

json
{
  "symbol": "AAPL",
  "price": "212.44",
  "change": "+1.86",
  "change_percent": "(+0.88%)"
}

From here the next steps are small: loop over a watchlist, write the records to CSV, or schedule the script to log a row per symbol at the close each day. For the fuller multi-field walkthrough, including historical data, see the scrape Yahoo Finance guide, and for the render-plus-trusted-IP approach on other JavaScript-heavy targets, see scraping JavaScript pages with Python.

Staying unblocked

Even with rendering handled, a financial site watches for scraper-shaped traffic. Three habits keep a longer watchlist run healthy: pace your requests with a short sleep between symbols so a tight loop does not get you throttled; lean on rotation, since a pool of residential IPs spreads requests across many addresses so no single one trips a rate limit (the Crawling API handles this for you); and read the status codes, treating a run that starts returning non-200 pc_status values as a signal to back off. For the broader playbook, see how to scrape websites without getting blocked.

Whether scraping Yahoo Finance is allowed depends on Yahoo's terms of service, your jurisdiction, and what you do with the data. Yahoo's terms restrict automated access and bulk collection, so a scraper can run against them regardless of how careful your tooling is. Read Yahoo's Terms of Service and the site's robots.txt, respect any rate expectations, and treat both as the boundary for what you collect. Stick to public market data: quotes, prices, the day's change, and the change percent are factual figures anyone can see without an account, which is the scope this script is built for. Do not pull anything behind a login or paywall, do not collect personal account data, and do not republish copyrighted editorial content. Where a project involves personal data, privacy rules such as GDPR and CCPA apply; a plain quote-price fetch like this one does not.

For production use, prefer an official market-data feed over scraping a consumer site. Much of what Yahoo Finance shows is licensed from market-data providers and exchanges with their own redistribution terms, so pulling a number off the page does not grant you the right to redistribute it. If you are building a product or need quotes at scale, license an official market-data API: that is the correct route for commercial or bulk use, and it gives you data you are actually permitted to redistribute.

Recap

Key takeaways

  • The quote page is client-side rendered. A plain request returns a shell with the live price missing, so you must render the page before you parse it.
  • You need rendering and a trusted IP together. The Crawling API with a JS token does both in one call; ajax_wait and page_wait control how long it waits for the price block.
  • Target the stable attributes. Read regularMarketPrice, regularMarketChange, and regularMarketChangePercent by their data-field attributes rather than Yahoo's generated class names.
  • Keep it small and loopable. One get_stock_price(symbol) function returns a dictionary; loop over a watchlist and pace the requests to track several symbols.
  • Stay on public market data. Respect Yahoo's ToS and robots.txt, avoid logins and personal data, and use an official market-data API for production or redistribution.

Frequently Asked Questions (FAQs)

Why does a plain request return a page without the live price?

Because Yahoo Finance renders its quote block client-side with JavaScript, and the price ticks in real time. The initial HTML is a shell that fills in only after the page's scripts run, so a raw HTTP request returns status 200 with the price field empty. To get the number you have to render the page first, which is what the Crawling API's JS token handles for you.

Do I need the normal token or the JS token for Yahoo Finance?

The JS token. The normal token fetches static HTML, which on a quote page is the same shell a plain fetch returns. The JS token renders the page in a real browser before handing back the HTML, so the price, change, and change percent are present when BeautifulSoup parses them.

How do I fetch prices for more than one ticker?

Put your symbols in a list and call get_stock_price for each, collecting the returned dictionaries. Add a short time.sleep between requests so you pace the run, and append each record to a list you can write out to JSON or CSV at the end.

My price field comes back as None. What changed?

Usually one of two things. Either the page had not finished rendering when it was captured, so raise page_wait by a second or two, or Yahoo moved the markup and the data-field attribute no longer matches. Open the live quote page in your browser's dev tools, find the element holding the price, and update the selector. Periodic selector maintenance is normal for any scraper.

Can I get historical prices with this script?

This script is scoped to the current quote: price, change, and change percent. Historical data lives on a different Yahoo Finance view with its own layout, so it needs different selectors. The fuller scrape Yahoo Finance guide covers more of the quote panel and is the right place to start for additional fields.

Can I use scraped Yahoo Finance data commercially?

Treat that as a legal question, not a technical one. Much of Yahoo Finance's data is licensed from market-data providers and exchanges with their own redistribution terms, and Yahoo's own Terms of Service restrict automated reuse, so commercial or bulk use generally needs permission. For a product or large-scale use, license an official market-data API and seek legal advice before building on top of the data.

Start Building

Crawl any site at scale, without fighting infrastructure.

Crawlbase handles proxies, fingerprints, and CAPTCHAs so your team ships data pipelines instead of maintaining crawl plumbing. 1,000 requests free, no card required.

Self-serve · No sales call required · Enterprise crawl volumes available