yfinance Suppress Output: Silence 404 and Progress Messages
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.
- 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.
| Symptom | Channel | Working fix | Why it works |
|---|---|---|---|
| '404 Client Error' or 'No data found' from Ticker(...) calls | logging, 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 stderr | redirect_stdout plus redirect_stderr around the call | print resolves sys.stdout at call time |
| Progress bar '[*****100%*****] 1 of 1 completed' | tqdm / progress output on stderr | download(..., progress=False) | The bar is never created |
| FutureWarning, DeprecationWarning | warnings module | warnings.filterwarnings('ignore', category=FutureWarning) | Filters the warning registry, not logging |
| Text survives every Python-level fix | File descriptors 1 or 2 (C layer, curl) | os.dup2 context manager | Redirects 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())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").recommendationsHow 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}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.