Howwise RSS
Trading Software7 min read

yfinance Suppress Output: Silence 404 and Progress Messages

Short answer

To yfinance suppress output, first find the channel: modern versions log through the 'yfinance' logger, so logging.getLogger('yfinance').setLevel(logging.CRITICAL) silences 404 messages, while download() also needs progress=False for the bar.

Key takeaways
  • Identify the channel first: run the script with 2>/dev/null, then with >/dev/null, and check yf.__version__ to see whether you have print() or logging behaviour.
  • On modern versions, logging.getLogger('yfinance').setLevel(logging.CRITICAL) silences the 404 and 'No data found' lines without touching the rest of your app.
  • redirect_stdout fails because the text goes to stderr, to a handler that captured the original stream, or to a thread or C layer that never looks at sys.stdout.
  • The durable fix is to catch exceptions, test for empty results, and keep a list of failed tickers, so the message becomes data instead of noise.
  • Never use logging.disable(logging.CRITICAL) or a permanent stderr redirect in long-running code, because rate-limit errors (429) look like 404s and you need to see them.

Which channel is the yfinance message actually using?

Silencing works only if you target the right channel. yfinance has used four: print() to stdout, print() to stderr, the logging module, and a tqdm progress bar (stderr). Warnings from Python or pandas are a fifth, separate channel.

Start with the version, because the behaviour changed across 0.2.x. Older releases printed messages such as 'HTTP Error 404' or '- XYZ: No data found, symbol may be delisted'. Newer ones send them to a logger named 'yfinance'.

Then use the shell to separate stdout from stderr. If the text vanishes with 2>/dev/null it is stderr, and if it vanishes with >/dev/null it is stdout. If it survives both, it is being written below Python.

python -c "import yfinance as yf; print(yf.__version__)"
pip show yfinance | head -3

# which stream? run your script twice
python myscript.py 2>/dev/null   # message gone -> it was stderr
python myscript.py >/dev/null    # message gone -> it was stdout

Which fix matches which output channel?

Most snippets online fix only one channel and look broken on another. Use this table after the diagnostic above.

If the message is a plain text line in a notebook cell, remember that Jupyter shows both stdout and stderr, so you cannot tell them apart visually. Use the shell test or inspect logging.getLogger('yfinance').handlers.

SymptomChannelWorking fixWhy it works
'404 Client Error' or 'No data found' from Ticker(...) callslogging, logger 'yfinance'logging.getLogger('yfinance').setLevel(logging.CRITICAL)The record is discarded before any handler sees it
Same text on very old versions (before the logger was added)print() to stdout or stderrredirect_stdout plus redirect_stderr around the callprint resolves sys.stdout at call time
Progress bar '[*****100%*****] 1 of 1 completed'tqdm / progress output on stderrdownload(..., progress=False)The bar is never created
FutureWarning, DeprecationWarningwarnings modulewarnings.filterwarnings('ignore', category=FutureWarning)Filters the warning registry, not logging
Text survives every Python-level fixFile descriptors 1 or 2 (C layer, curl)os.dup2 context managerRedirects below Python's sys.stdout object

How do I make yfinance suppress output without hiding my own logs?

On modern versions, target only the yfinance logger. Everything else in your application keeps logging normally.

yf.utils.get_yf_logger() returns the same logger object as logging.getLogger('yfinance') in versions that have it, so the stdlib call is the safer choice across versions.

If you want other messages (for example a rate-limit warning) but not the missing-data noise, add a filter instead of raising the level.

Match the phrases precisely. A bare '404' substring also matches unrelated text, such as a price like 1404.25, a request ID, or a ticker or port number inside a message, so a loose filter can hide a message you needed. The pattern below only matches 404 when it appears as an HTTP error, and it never matches 429 rate-limit messages.

import logging
import re
import yfinance as yf

# Option 1: only critical messages get through
logging.getLogger("yfinance").setLevel(logging.CRITICAL)

# Option 2: fully off
logging.getLogger("yfinance").disabled = True

# Option 3: drop only the noisy missing-data lines
class DropMissingData(logging.Filter):
    # "404" is only matched as an HTTP status, never as a bare substring
    PATTERN = re.compile(
        r"HTTP Error 404\b"
        r"|\b404 Client Error\b"
        r"|No data found"
        r"|possibly delisted"
        r"|Quote not found",
        re.IGNORECASE,
    )
    def filter(self, record):
        return not self.PATTERN.search(record.getMessage())

for h in logging.getLogger("yfinance").handlers:
    h.addFilter(DropMissingData())
# if the logger has no handlers of its own, attach to your root handler instead:
for h in logging.getLogger().handlers:
    h.addFilter(DropMissingData())
Warning: Do not call logging.disable(logging.CRITICAL). It is process-wide and mutes every library and your own code, including the errors you will want when something real breaks. Also check the exact wording your installed version logs, since a filter pattern that is too loose hides real errors and one that is too strict hides nothing.

Why does redirect_stdout not silence yfinance?

redirect_stdout only replaces the sys.stdout name for the duration of the with block. It cannot catch anything that never reads that name, and the yfinance 404 usually does not.

Common reasons it fails: the message goes to stderr (you need redirect_stderr too); it is emitted by a logging.StreamHandler that stored the original sys.stderr object when it was created, so swapping sys.stderr later changes nothing; or it is emitted from a worker thread or a C library.

Redirects also mutate a global, so they are not thread-safe. If another thread prints inside the block, its output is swallowed too, and two threads entering and exiting blocks can restore the wrong stream.

For old print()-based versions, both redirects together are the correct tool:

import contextlib, io
import yfinance as yf

buf = io.StringIO()
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
    df = yf.Ticker("ABCDW").history(period="1mo")

# buf.getvalue() now holds what yfinance printed; inspect or log it yourself
if "No data found" in buf.getvalue():
    print("missing:", "ABCDW")

Why does yfinance.download still print a progress bar?

download() fetches tickers in a thread pool and draws a tqdm bar on stderr. Output from worker threads is not tied to your with block timing, and tqdm writes to the stderr object it grabbed, so redirects are unreliable.

Pass progress=False to remove the bar. Add threads=False when you need deterministic ordering and capture, at the cost of speed. For general tqdm control in your own loops, see the separate tqdm article rather than patching yfinance.

Per-ticker failure messages from download() go through the logger on modern versions, so combine the logger fix with progress=False.

Keep warnings separate. FutureWarning and DeprecationWarning noise (often from pandas, or from yfinance's changed defaults such as auto_adjust) is a different channel and should be filtered by category, not by silencing the error logger.

import warnings, logging
import yfinance as yf

warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"yfinance.*")
logging.getLogger("yfinance").setLevel(logging.CRITICAL)

data = yf.download(
    ["AAPL", "XYZW", "NEWIPO"],
    period="1mo",
    progress=False,
    threads=False,        # optional: deterministic, slower
    auto_adjust=True,     # set explicitly to avoid default-change warnings
)

What is a safe quiet_yf() helper, and what if output comes from below Python?

A context manager keeps the change scoped and guarantees restoration. The default touches only the logger. The stream redirect is opt-in, because a blanket stderr redirect hides genuine tracebacks printed inside the block.

If text still appears, it is written at file descriptor level (C extensions, libcurl via curl_cffi). Then the only option is os.dup2 on descriptors 1 and 2. This is process-wide, affects every thread, and is risky in servers, so keep the block short.

Version caveat: the logger name is stable on 0.2.x, but older print-based releases ignore it, so use streams=True there.

import contextlib, io, logging, os, sys

@contextlib.contextmanager
def fd_silence():
    sys.stdout.flush(); sys.stderr.flush()
    saved = [os.dup(1), os.dup(2)]
    devnull = os.open(os.devnull, os.O_WRONLY)
    try:
        os.dup2(devnull, 1); os.dup2(devnull, 2)
        yield
    finally:
        os.dup2(saved[0], 1); os.dup2(saved[1], 2)
        for fd in (*saved, devnull):
            os.close(fd)

@contextlib.contextmanager
def quiet_yf(streams=False, fds=False):
    log = logging.getLogger("yfinance")
    old_level, old_disabled = log.level, log.disabled
    log.setLevel(logging.CRITICAL)
    try:
        if fds:
            with fd_silence():
                yield
        elif streams:
            with contextlib.redirect_stdout(io.StringIO()), \
                 contextlib.redirect_stderr(io.StringIO()):
                yield
        else:
            yield
    finally:
        log.setLevel(old_level)
        log.disabled = old_disabled

# usage
with quiet_yf():
    rec = yf.Ticker("ABCD.U").recommendations
Warning: Descriptor-level redirection also swallows real tracebacks and output from other threads while active. Never leave it enabled around long-running or multi-threaded code.

How should I handle tickers with no data instead of hiding the message?

SPAC units, warrants, rights and recent listings legitimately lack quoteSummary modules such as analyst recommendations, so the 404 is expected, not a bug. Hiding it is fine only if the missing result is captured explicitly.

Wrap each call, treat None or an empty DataFrame as a miss, return a sentinel, and print one summary line at the end. Cache known misses (a set persisted to disk) so you do not request them again.

If you pre-filter symbols, base the filter on security type, never on how the ticker is spelled. Identify SPAC units, warrants and rights from a reference list (for example the Nasdaq Trader symbol directory files, which include a security name and ETF/test-issue flags) or from the exchange's documented symbol format for those instruments. Then skip only the symbols that list classifies as a unit, warrant or right.

Do not exclude tickers by default just because they end in W, WS, U or R. Many ordinary common stocks end in those letters, for example SNOW, UBER and ROKU, and a literal suffix rule would silently drop them from your results. Suffix conventions also differ between exchanges and Yahoo Finance, so check a sample of the filtered symbols before trusting the rule.

Beware that a burst of 429 (rate limited) responses can look like a burst of 404s in a loop. If every ticker fails suddenly, you are probably throttled, so add a short time.sleep, reuse a session cache such as requests-cache, and do not silence the logger while diagnosing. Newer versions raise a rate-limit exception type (YFRateLimitError) that you can catch separately.

import time, logging
import pandas as pd
import yfinance as yf
from requests.exceptions import HTTPError

try:
    from yfinance.exceptions import YFException
except ImportError:            # older versions
    class YFException(Exception): pass

log = logging.getLogger("mytool")
MISSING = None                  # sentinel

# Built from a reference list by security type (unit / warrant / right),
# NOT by matching the last letters of the ticker.
KNOWN_NO_DATA = set()

def get_recs(symbol):
    try:
        df = yf.Ticker(symbol).recommendations
    except (HTTPError, YFException) as exc:
        log.info("%s: %s", symbol, type(exc).__name__)
        return MISSING
    if df is None or df.empty:
        return MISSING
    return df

failed, results = [], {}
for sym in symbols:
    if sym in KNOWN_NO_DATA:
        failed.append(sym)
        continue
    out = get_recs(sym)
    (failed.append(sym) if out is MISSING else results.__setitem__(sym, out))
    time.sleep(0.3)

log.warning("%d/%d tickers had no data: %s", len(failed), len(symbols), ", ".join(failed))

# after yf.download(), failures are recorded here (private API, may change):
import yfinance.shared as shared
bad = dict(getattr(shared, "_ERRORS", {}))   # {ticker: message}
Tip: yfinance.shared._ERRORS is private and has changed between releases. Read it with getattr as above, and fall back to checking which columns of the download result are all NaN.

Common questions

How do I set the yfinance logging level to show only real errors?

Call logging.getLogger('yfinance').setLevel(logging.ERROR) to keep error records, or CRITICAL to drop nearly everything. Because missing-data 404s may be logged at ERROR, a filter on the message text is the more precise choice.

Does progress=False hide the 404 messages from yf.download?

No. It only removes the tqdm bar. The per-ticker failure lines are separate log records, so you still need the logger level change or a filter.

Why does yfinance say 'Quote not found' or 'possibly delisted' for a valid symbol?

The symbol often exists but lacks the requested data, as with warrants, units, or a listing too new to have history. It can also appear when a request was throttled or the symbol format is wrong, for example a class share needing a dash (BRK-B) rather than a dot.

Can I capture the suppressed messages instead of discarding them?

Yes. Attach a logging.Handler subclass (or a StreamHandler writing to io.StringIO) to the 'yfinance' logger and read the records afterwards. This gives you the text per ticker without printing it.

Will silencing the logger break anything in yfinance?

No, logging has no effect on control flow, so data fetching and return values are unchanged. Exceptions are still raised where the library raises them, so your try/except logic keeps working.

Related topics:yfinancepythonloggingerror-handlingmarket-data