Howwise RSS
Trading Software8 min read

yfinance Not Working: Fix 429, Empty Data and KeyError

Short answer

If yfinance is not working, upgrade first with pip install -U yfinance, then match your symptom to its cause: empty data, 401 crumb errors, 429 rate limits or missing .info keys. Yahoo's endpoints are unofficial and change without notice.

Key takeaways
  • Run pip install -U yfinance and print yfinance.__version__ first, because most outages are fixed by a new release within days.
  • History uses the unauthenticated chart endpoint, while .info and fundamentals need cookie and crumb authentication, so .info fails more often.
  • 429 and YFRateLimitError come from request volume or shared IPs, so batch with yf.download, cache to disk and back off exponentially.
  • Use .get() with defaults for .info fields, since ETFs, indices, crypto and foreign tickers return different keys.
  • yfinance and yahoo_fin are unofficial and will never announce a deprecation, so put them behind an interface with a fallback provider.

Why is yfinance not working when it worked yesterday?

yfinance and yahoo_fin are unofficial scrapers. They call Yahoo's undocumented web endpoints, the same ones Yahoo's own pages use. There is no API contract, no SLA and no deprecation notice. Yahoo can change cookies, crumb tokens, consent flows, response shapes or rate limits at any time, and every client breaks until a maintainer patches it.

That is why the same script can fail on one day, work after an upgrade, and fail again for one machine only. Throttling is also per IP and per client fingerprint. Recent yfinance versions use curl_cffi to impersonate a real browser's TLS fingerprint, because plain Python HTTP clients are easier to block.

The first step is always the same: upgrade, then check the version you actually run, since a stale virtualenv or notebook kernel is a common cause. Then look at the project's GitHub issues page for reports matching your error text. If others reported it in the last few days, wait for the release instead of debugging.

pip install -U yfinance curl_cffi
python -c "import yfinance; print(yfinance.__version__)"
# if pip says 'already satisfied' but behaviour is old, you are in another environment:
python -m pip show yfinance
Warning: Upgrading can change return shapes (for example column layouts from yf.download or auto_adjust defaults). Pin the version in requirements.txt once a version works, and test before upgrading production jobs.

Why does .history() work while .info fails?

They hit different endpoints. Price history comes from Yahoo's chart API, which needs little authentication. The .info dict and most fundamentals come from quoteSummary-style endpoints that require a session cookie plus a crumb token. The client must fetch the cookie (sometimes passing a consent page), then request a crumb, then attach both to each call.

Every step in that chain can fail: an expired crumb gives 401 'Invalid Crumb', a flagged IP gives 429, and a consent redirect gives an unexpected HTML page that cannot be parsed. History skips most of that chain, so it keeps working through many outages.

Also, .info returns whatever Yahoo has for that symbol. Some tickers return a partial dict with only a few keys, which later surfaces as a KeyError in your code rather than a network error.

Tip: If you only need the last price, market cap or 52-week range, use Ticker.fast_info. It relies on lighter calls and is usually more reliable than the full .info dict.

Which error do I have? Symptom, cause and fix

Each failure has a different cause, so match the exact message before changing anything.

SymptomLikely causeFix
Empty DataFrame plus 'possibly delisted; no price data found'Wrong or renamed symbol, missing exchange suffix (.L, .TO, .DE), a period with no trading, or a throttled request that Yahoo answered with nothingCheck the symbol on Yahoo's site, add the suffix, try period='5d', then retry from another network
HTTPError 401 'Invalid Crumb' or 'Unauthorized'Stale cookie or crumb, or consent flow not completedUpgrade yfinance and curl_cffi, restart the process so a new session is made, avoid reusing old sessions
HTTPError 404Symbol or endpoint does not exist for that instrument (common for indices, crypto, delisted tickers on fundamentals calls)Verify the symbol, fall back to history() for that instrument
429 'Too Many Requests' or YFRateLimitErrorToo many requests from your IP, often a loop calling .info per ticker, or a shared cloud IPBatch with yf.download, add sleeps and backoff, cache, wait 15 to 60 minutes, test from a home network
JSONDecodeError or 'Expecting value'Yahoo returned an HTML block, consent or error page instead of JSONPrint the raw response status if possible, upgrade, check for IP blocking or a proxy rewriting traffic
KeyError: 'trailingPE' or 'sector'The ticker's .info dict lacks that field (ETF, index, crypto, foreign listing), or a partial responseUse info.get('key'), see the missing-fields section below

How do I run a quick diagnostic script?

This script isolates the layer that fails. If history passes and info fails, you have a crumb or throttle problem, not a broken install. If everything fails with the same exception, suspect the network or the library version.

import sys
import yfinance as yf

print("python", sys.version.split()[0], "| yfinance", yf.__version__)
try:
    import curl_cffi
    print("curl_cffi", curl_cffi.__version__)
except ImportError:
    print("curl_cffi NOT installed")

def probe(name, fn):
    try:
        print(f"{name:12} OK   {fn()}")
    except Exception as e:
        print(f"{name:12} FAIL {type(e).__name__}: {str(e)[:160]}")

t = yf.Ticker("AAPL")
probe("history", lambda: t.history(period="5d").shape)
probe("fast_info", lambda: t.fast_info["lastPrice"])
probe("info", lambda: len(t.info))
probe("income_stmt", lambda: t.get_income_stmt().shape)

How do I fix yfinance 429 Too Many Requests and YFRateLimitError?

Throttling is driven by request count and pattern. A loop that calls Ticker(sym).info for 500 symbols makes 500 or more authenticated requests in seconds, which is the classic trigger. Cloud and shared IPs (Colab, AWS, GitHub Actions and other CI runners, VPNs) are often blocked or throttled before you send a single request, because thousands of other users share them. Test from a home or office connection to separate a code problem from an IP problem.

Do not pass a requests-cache session to yfinance. Recent versions expect a curl_cffi based session and may reject other session types, so cache the results you get back instead.

The pattern below downloads in chunks with one batched call per chunk, retries with exponential backoff and jitter, refuses to accept empty output, and stores each chunk as parquet so reruns cost zero requests.

import time, random, hashlib, pathlib
import pandas as pd
import yfinance as yf

CACHE = pathlib.Path("cache"); CACHE.mkdir(exist_ok=True)

def _download(chunk, period, retries=5):
    for i in range(retries):
        try:
            df = yf.download(chunk, period=period, auto_adjust=True,
                             group_by="ticker", threads=True, progress=False)
            if df is not None and not df.empty:
                return df
            raise ValueError("empty result")
        except Exception as e:
            wait = min(2 ** i * 5, 120) + random.uniform(0, 3)
            print(f"{type(e).__name__}: retry {i+1} in {wait:.0f}s")
            time.sleep(wait)
    raise RuntimeError(f"gave up on {chunk[:3]}...")

def prices(tickers, period="1y", chunk_size=50, max_age_h=12):
    frames = []
    for i in range(0, len(tickers), chunk_size):
        chunk = tickers[i:i + chunk_size]
        key = hashlib.md5((",".join(chunk) + period).encode()).hexdigest()
        f = CACHE / f"{key}.parquet"
        if f.exists() and time.time() - f.stat().st_mtime < max_age_h * 3600:
            frames.append(pd.read_parquet(f)); continue
        df = _download(chunk, period)
        df.to_parquet(f)
        frames.append(df)
        time.sleep(random.uniform(2, 5))  # pause between chunks
    return pd.concat(frames, axis=1)
Warning: Retrying fast after a 429 usually extends the block. Back off for minutes, not milliseconds, and stop the loop if several chunks in a row fail.

Why do .info keys go missing, and what about yahoo_fin?

An ETF has no trailingPE, an index has no sector, and crypto has no balance sheet. Foreign tickers often lack fields too. Write code that expects absence, and treat a very short dict as a failed fetch rather than as data.

For statements, prefer get_income_stmt(), get_balance_sheet(), get_cashflow() or the quarterly_financials attribute over parsing .info. They return DataFrames with line items as rows, and a missing row is a normal condition you can handle.

yahoo_fin is far less maintained. It scrapes HTML pages and old endpoints, so many functions now return errors or empty results, and a fix may never arrive. Move to yfinance or another provider rather than patching it.

info = t.info or {}
if len(info) < 5:
    raise RuntimeError("partial .info, treat as failed fetch")
pe = info.get("trailingPE")            # None for ETFs, losses, indices
name = info.get("longName") or info.get("shortName", "n/a")
price = t.fast_info.get("lastPrice") if hasattr(t.fast_info, "get") else t.fast_info["lastPrice"]

What is a reliable alternative to yfinance, and how should I design for failure?

Yahoo data is generally for personal use. Commercial or production use needs a licensed feed, so check the terms of whichever source you rely on. Free-tier limits change often, so treat the figures below as approximate as of late 2026 and verify them on each provider's pricing page before building around them.

Then insulate your code. Put all data access behind one interface, validate every response, retry with backoff, and keep a second provider. Then an outage becomes a log line instead of a broken pipeline.

SourceBest forFree-tier shape (verify)Catch
Alpha VantageDaily and intraday prices, some fundamentalsSmall daily request cap (tens per day)Too few calls for wide universes
Twelve DataPrices, many marketsPer-minute and per-day credit limits (single-digit calls per minute)Fundamentals and some exchanges are paid
Polygon (now Massive)US equities, clean REST and websocketsA few calls per minute, delayed or end-of-day dataReal-time and deep history need paid plans
TiingoAdjusted end-of-day historyHourly and daily request capsFundamentals limited on free tier
Financial Modeling PrepFundamentals and statementsDaily call cap in the low hundredsMany endpoints gated by plan
Stooq via pandas-datareaderFree daily OHLC, no keyNo key, informal limitsDaily only, symbol naming differs, may break
SEC EDGAR APIOfficial US filings and XBRL fundamentalsAbout 10 requests per second, descriptive User-Agent requiredRaw XBRL concepts need mapping, no prices
Broker APIPrices tied to your accountAccount-based, rate-limitedNeeds an account, terms restrict redistribution
  1. Define a small interface, such as get_prices(symbol, start, end) returning a DataFrame with fixed columns and a UTC or exchange-aware index.
  2. Implement one class per provider and normalise symbols, column names and adjusted versus unadjusted prices inside it.
  3. Validate each result: non-empty, expected columns, monotonic index, no NaN in close, last date within the expected trading window.
  4. Try providers in order with exponential backoff, log which one served the data, and cache successful results.
  5. Do not mix adjusted prices from one provider with unadjusted prices from another in the same series.
class PriceProvider:
    def get_prices(self, symbol, start, end):
        raise NotImplementedError

def get_with_fallback(providers, symbol, start, end):
    errors = []
    for p in providers:
        try:
            df = p.get_prices(symbol, start, end)
            if df is not None and not df.empty and df["close"].notna().all():
                return df
            errors.append((type(p).__name__, "invalid data"))
        except Exception as e:
            errors.append((type(p).__name__, repr(e)))
    raise RuntimeError(f"all providers failed for {symbol}: {errors}")

Common questions

Why does yfinance return an empty DataFrame with 'possibly delisted'?

The message is generic and also appears when the symbol is wrong, lacks its exchange suffix, or the request was throttled and returned nothing. Test a known symbol like AAPL with period='5d'. If that works, fix your symbol; if it also fails, suspect version or IP throttling.

How long does a yfinance 429 block last?

There is no published duration. Blocks often clear after a period ranging from several minutes to a few hours, but a shared cloud IP may stay blocked. Stop sending requests while blocked, since continued retries can prolong it.

Is yfinance deprecated or shut down?

There is no official deprecation, because Yahoo never offered yfinance as a product. It is a community project that depends on endpoints Yahoo can change at any time. Pin a working version and keep a fallback source.

Can I use yfinance data in a commercial product?

Yahoo's data is intended for personal use, and redistributing it or building a paid product on it generally requires a licensed feed. Read the terms of any provider you use and get a licensed source for production.

Should I use a proxy or rotate IPs to avoid 429 errors?

Rotating IPs to evade limits is fragile and may breach the provider's terms. Reducing the request count with batching, caching and fewer .info calls fixes most cases, and a supported API is the sturdier answer if you need volume.

Related topics:yfinancepythonmarket datarate limitingyahoo finance