DEV Community

Synergic-Apis
Synergic-Apis

Posted on

yfinance NG=F Not Working? Why Natural Gas Futures Data Fails and 3 Fixes That Work

If your script suddenly started printing this:

>>> import yfinance as yf
>>> df = yf.download("NG=F", period="1mo")

1 Failed download:
['NG=F']: YFPricesMissingError('possibly delisted; no price data found')
Enter fullscreen mode Exit fullscreen mode

…you didn't break anything. NG=F (the natural gas futures ticker on Yahoo Finance) periodically stops returning data for everyone, and futures tickers get hit harder than stocks. This post covers why it happens and the three fixes that actually work, ordered from "quick patch" to "never deal with this again."

1. What the error actually means

yfinance is not an official API. It's a (great) community library that scrapes Yahoo Finance's internal endpoints — the same ones Yahoo's own website uses. Yahoo doesn't document them, doesn't promise they'll keep working, and changes them whenever it suits their frontend.

When Yahoo changes something — an endpoint, a rate limit, a response format — yfinance breaks until its maintainers reverse-engineer the change. Futures symbols like NG=F and GC=F are the most fragile: they've had recurring gaps and failures reported over the years, for example #2620 (missing recent data for NG=F/GC=F), #2635 (whole missing days in futures history) and the evergreen #865 "Futures only work sometimes".

So: "possibly delisted" almost never means delisted. It means "the scrape came back empty."

2. Fix #1 — the quick patches (works today, breaks tomorrow)

Three things fix most transient failures:

Upgrade first. The maintainers usually patch Yahoo changes within days:

pip install -U yfinance
Enter fullscreen mode Exit fullscreen mode

Retry with backoff. Failures are often intermittent rate-limiting, not hard breaks:

import time
import yfinance as yf

def download_with_retry(ticker, retries=3, wait=5, **kwargs):
    for attempt in range(1, retries + 1):
        df = yf.download(ticker, progress=False, **kwargs)
        if not df.empty:
            return df
        print(f"attempt {attempt} came back empty, retrying in {wait}s…")
        time.sleep(wait * attempt)
    raise RuntimeError(f"{ticker}: no data after {retries} attempts")

df = download_with_retry("NG=F", period="1mo")
Enter fullscreen mode Exit fullscreen mode

Try the other code path. yf.download() and yf.Ticker().history() don't always behave identically for futures (see #1036):

df = yf.Ticker("NG=F").history(period="1mo")
Enter fullscreen mode Exit fullscreen mode

If none of that works, it's a real breakage — wait for a patch, or keep reading.

3. Why it will keep breaking

This isn't a bug you fix once. It's structural:

  • Unofficial: Yahoo owes yfinance nothing, and scraping is against Yahoo's terms of service — which also means you can't legitimately ship yfinance data inside a commercial product.
  • Futures are second-class: most of Yahoo's traffic is stocks; futures endpoints get less love and break more.
  • Your pipeline inherits the risk: if a cron job, dashboard or trading signal depends on NG=F, every silent Yahoo change is now your incident.

For a weekend backtest, that trade-off is fine. For anything that runs unattended every day, you eventually want a data source whose job is to keep answering.

4. Fix #2 — a drop-in replacement (one line to migrate)

I got tired of this cycle and moved my pipelines to a dedicated natural gas price API. The function below returns a DataFrame shaped like what you're used to (date index, Close column), so migrating is one line:

import requests
import pandas as pd

RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"  # free key, no card: see link at the end

def get_natural_gas(start=None, end=None):
    """Drop-in replacement for yf.download('NG=F'):
    returns a DataFrame indexed by date with a Close column."""
    url = "https://natural-gas-price-api.p.rapidapi.com/historical"
    headers = {
        "X-RapidAPI-Key": RAPIDAPI_KEY,
        "X-RapidAPI-Host": "natural-gas-price-api.p.rapidapi.com",
    }
    params = {}
    if start:
        params["start"] = start
    if end:
        params["end"] = end
    r = requests.get(url, headers=headers, params=params, timeout=10)
    r.raise_for_status()
    rows = r.json()["data"]
    df = pd.DataFrame(rows).rename(columns={"date": "Date", "price": "Close"})
    df["Date"] = pd.to_datetime(df["Date"])
    return df.set_index("Date").sort_index()

# before:
# df = yf.download("NG=F", start="2025-01-01")
# after:
df = get_natural_gas(start="2025-01-01")
Enter fullscreen mode Exit fullscreen mode

One honest nuance before you swap blindly: NG=F is the front-month futures contract; a price API typically serves the spot price (e.g. Henry Hub). They track each other closely but are not the same number — spot is the right series for "what does gas cost today" analytics and dashboards, while futures matter if you're modeling contract roll or term structure. Know which one your use case needs.

5. Fix #3 — the free official source (EIA), if you only need US history

Full honesty: if all you need is official US historical data and you don't mind some assembly, the U.S. Energy Information Administration gives it away for free. Register for a key at eia.gov/opendata, then:

import requests
import pandas as pd

EIA_KEY = "YOUR_FREE_EIA_KEY"

url = "https://api.eia.gov/v2/natural-gas/pri/fut/data/"
params = {
    "api_key": EIA_KEY,
    "frequency": "daily",
    "data[0]": "value",
    "facets[series][]": "RNGWHHD",  # Henry Hub daily spot
    "sort[0][column]": "period",
    "sort[0][direction]": "desc",
    "length": 5000,
}
resp = requests.get(url, params=params, timeout=30).json()
df = pd.DataFrame(resp["response"]["data"])[["period", "value"]]
df.columns = ["Date", "Close"]
df["Date"] = pd.to_datetime(df["Date"])
df = df.set_index("Date").sort_index()
Enter fullscreen mode Exit fullscreen mode

The catches, so you're not surprised: data is published with a delay (it's a statistics agency, not a market feed), it's US-only (no European TTF), responses are capped at 5,000 rows per request, and the v2 route/facet system takes a while to learn. Great archival source; not a live one.

6. Choosing between the three

yfinance (NG=F) EIA Open Data Dedicated API
What it is Scraper of Yahoo's internal endpoints Official US government statistics Commercial API with a stable contract
Stability Breaks when Yahoo changes things Very stable Uptime is the product
OK to ship in production? No (unofficial, against ToS) Yes (public domain) Yes
Series Front-month futures OHLC Henry Hub spot + storage, US only Spot + historical
Freshness Near-real-time, when it works Published with a lag Frequent updates
Europe (TTF) Fragile symbols at best No Yes
Effort pip install Key + learn v2 routes Key + one GET
Cost Free Free Free tier, paid for volume

7. The resilient version (primary + fallback)

What I actually run in daily jobs — a stable primary source with yfinance demoted to fallback duty:

"""Daily natural gas price fetch that doesn't die when Yahoo changes something."""
import sys

def fetch_price():
    try:
        df = get_natural_gas()  # primary: stable API (section 4)
        source = "natural-gas-price-api"
    except Exception as api_err:
        print(f"[warn] primary API failed: {api_err}", file=sys.stderr)
        try:
            import yfinance as yf
            df = yf.download("NG=F", period="5d", progress=False)
            if df.empty:
                raise ValueError("yfinance returned an empty frame")
            source = "yfinance (fallback)"
        except Exception as yf_err:
            raise RuntimeError(f"all sources failed: {api_err} / {yf_err}")
    print(f"latest close ({source}): {float(df['Close'].iloc[-1]):.3f}")
    return df

if __name__ == "__main__":
    fetch_price()
Enter fullscreen mode Exit fullscreen mode

Two sources, loud warnings, no silent empty DataFrames poisoning your pipeline.


Full disclosure: the API used in sections 4 and 7 is mine — I built natural-gas-price-api after hitting exactly this problem. There's a free tier (no card required) if you want to try it, and the EIA route above is genuinely good if free official US history is all you need. Questions about data sourcing welcome in the comments.

Top comments (0)