How to Master Build Stock Screener Python [Step-by-Step Guide]
To build a stock screener in Python, you write a script that pulls fundamental data, applies a set of numerical filters, ranks the survivors, and saves the results. This guide walks through each step with working code, from data sourcing to an automated output you can run weekly without touching the script again.
The Python approach requires more setup than a ready-made tool like the ValueMarkers screener, but it gives you complete control over filter logic and output format. Both approaches produce better results when you understand the underlying methodology.
Key Takeaways
- Build stock screener Python projects start with three choices: data source, filter criteria, and output format. Get those right before writing any code.
- yfinance is the fastest way to start. It is free, requires no API key, and returns the core fundamental fields for most U.S. and major international tickers.
- ROIC, debt-to-equity, and ROE are the three filters that most cleanly separate quality businesses from mediocre ones across historical periods.
- A composite ranking function scores survivors on multiple criteria simultaneously, which is more useful than a simple pass/fail filter output.
- Automating with a GitHub Actions cron job lets your screen run on a schedule without a local machine staying on.
- Always validate your script's output against a trusted data source before relying on it. API data varies in accuracy, particularly for smaller-cap names.
Environment Setup
You need Python 3.9 or later. Install the required libraries with one command.
pip install yfinance pandas requests scipy
No API key is required for yfinance. ROIC is not in the .info dictionary directly; you calculate it from the income statement and balance sheet, which is what this guide does.
Step 1: Build the Ticker List
Use the S&P 500 Wikipedia table as a free, auto-updating ticker source.
import pandas as pd
def get_sp500_tickers():
url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
df = pd.read_html(url)[0]
tickers = df["Symbol"].str.replace(".", "-", regex=False).tolist()
return tickers
tickers = get_sp500_tickers()
print(f"Loaded {len(tickers)} tickers")
For a global screen, replicating 73 exchanges in Python requires a paid data provider or significant manual maintenance.
Step 2: Fetch Fundamentals and Calculate ROIC
Fetch the .info dictionary and financial statement data for each ticker. ROIC requires operating income from the income statement and total debt and equity from the balance sheet.
import yfinance as yf
import time
def get_stock_data(ticker):
try:
stock = yf.Ticker(ticker)
info = stock.info
# Try to calculate ROIC from financial statements
income_stmt = stock.income_stmt
balance_sheet = stock.balance_sheet
roic = None
if not income_stmt.empty and not balance_sheet.empty:
try:
operating_income = income_stmt.loc["Operating Income"].iloc[0]
total_debt = balance_sheet.loc["Total Debt"].iloc[0]
total_equity = balance_sheet.loc["Stockholders Equity"].iloc[0]
invested_capital = total_debt + total_equity
if invested_capital > 0:
tax_rate = 0.21 # approximate U.S. corporate tax rate
nopat = operating_income * (1 - tax_rate)
roic = nopat / invested_capital
except (KeyError, IndexError):
roic = None
return {
"ticker": ticker,
"name": info.get("longName", ""),
"sector": info.get("sector", ""),
"pe_ratio": info.get("trailingPE"),
"roe": info.get("returnOnEquity"),
"debt_to_equity": info.get("debtToEquity"),
"roic": roic,
"revenue_growth": info.get("revenueGrowth"),
"market_cap": info.get("marketCap"),
}
except Exception as e:
print(f"Error: {ticker} - {e}")
return None
records = []
for ticker in tickers[:100]:
data = get_stock_data(ticker)
if data:
records.append(data)
time.sleep(0.4)
df = pd.DataFrame(records)
The ROIC calculation uses NOPAT (net operating profit after tax) divided by invested capital. This matches the standard definition: it measures the after-tax return on the capital the business has deployed, regardless of how that capital was financed.
Step 3: Apply the Core Filters
With the DataFrame populated, apply your filters. Build stock screener Python logic with pandas boolean indexing.
def apply_filters(df):
df = df.copy()
# Convert D/E: yfinance returns it as percentage in some versions
# Check your data: if Apple (AAPL) shows D/E ~150, divide by 100
df["de_ratio"] = df["debt_to_equity"] / 100
filtered = df[
(df["pe_ratio"].notna()) &
(df["roe"].notna()) &
(df["de_ratio"].notna()) &
(df["pe_ratio"] > 0) &
(df["pe_ratio"] < 22) & # Valuation filter
(df["roe"] > 0.10) & # Quality filter: ROE above 10%
(df["de_ratio"] < 1.0) & # Health filter: D/E below 1.0x
]
# Add ROIC filter if data is available
if df["roic"].notna().sum() > 10:
filtered = filtered[
filtered["roic"].isna() | # Include if ROIC unavailable
(filtered["roic"] > 0.10) # ROIC above 10% if available
]
return filtered.reset_index(drop=True)
df_filtered = apply_filters(df)
print(f"Passed filters: {len(df_filtered)} stocks")
The ROIC filter uses a conditional: include the stock if ROIC is unavailable rather than excluding it. This prevents data gaps from silently eliminating valid candidates.
Step 4: Rank the Results
Pass/fail output lists are less useful than ranked lists. A composite score reveals which survivors look best on the combined criteria.
from scipy.stats import rankdata
import numpy as np
def rank_results(df):
df = df.copy()
# Rank each metric (higher rank = better)
df["rank_pe"] = rankdata(-df["pe_ratio"].fillna(999)) # Lower P/E = better
df["rank_roe"] = rankdata(df["roe"].fillna(0)) # Higher ROE = better
df["rank_de"] = rankdata(-df["de_ratio"].fillna(999)) # Lower D/E = better
# ROIC rank if available
if df["roic"].notna().sum() > 5:
median_roic = df["roic"].median()
df["roic_filled"] = df["roic"].fillna(median_roic)
df["rank_roic"] = rankdata(df["roic_filled"])
else:
df["rank_roic"] = 1 # Neutral if no ROIC data
# Weighted composite score
df["composite_score"] = (
df["rank_pe"] * 0.30 +
df["rank_roe"] * 0.35 +
df["rank_de"] * 0.15 +
df["rank_roic"] * 0.20
)
return df.sort_values("composite_score", ascending=False)
df_ranked = rank_results(df_filtered)
The weights (30% P/E, 35% ROE, 15% D/E, 20% ROIC) reflect a quality-value tilt. A pure deep-value approach would increase the P/E weight to 50% and reduce the ROE weight accordingly.
Step 5: Output and Compare Against Benchmarks
Save the top results and display a clean summary table.
output_cols = ["ticker", "name", "sector", "pe_ratio", "roe", "de_ratio", "roic", "composite_score"]
top20 = df_ranked[output_cols].head(20)
print(top20.to_string(index=False))
top20.to_csv("screener_output.csv", index=False)
Before trusting this output, validate it against known benchmarks. Apple's ROE from your script should be close to 160% (the high level that results from massive buybacks reducing book equity). Microsoft's ROE should be around 35 to 40%. If these diverge significantly from those ranges, investigate whether yfinance is returning stale or incorrectly scaled data.
| Metric | Apple (AAPL) | Microsoft (MSFT) | JNJ |
|---|---|---|---|
| P/E ratio | ~28.3 | ~32.1 | ~15.4 |
| ROE | ~160% | ~35% | ~25% |
| ROIC | ~45.1% | ~35.2% | ~18% |
| Debt-to-equity | ~1.8x | ~0.4x | ~0.5x |
Note that AAPL's high D/E reflects deliberate financial engineering, not distress. Apply D/E filters with sector context in mind.
Step 6: Automate with GitHub Actions
To run your screen weekly without a local machine, use a GitHub Actions workflow. Create a file at .github/workflows/screener.yml in your repository.
name: Weekly Stock Screen
on:
schedule:
- cron: "0 21 * * 5" # Fridays at 9:00 PM UTC (4:00 PM Eastern)
workflow_dispatch:
jobs:
run-screener:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- run: pip install yfinance pandas scipy
- run: python screener.py
- uses: actions/upload-artifact@v3
with:
name: screener-results
path: screener_output.csv
This workflow runs every Friday at 9:00 PM UTC and uploads the CSV as a downloadable artifact. You receive a GitHub email when the run completes.
When to Use Python vs. a Dedicated Screener
Building a Python screener is worth the investment if you need logic a UI cannot express: custom composite formulas, sector-relative thresholds, or integration with your own portfolio database. For most investors, the ValueMarkers screener handles the job in minutes without code. It covers 120+ indicators including ROIC, runs across 73 global exchanges, and avoids the data-gap problem that affects free Python libraries. Use Python for customization the UI cannot provide. Use the screener when speed and coverage matter more.
Why python screener tutorial Matters
This section anchors the discussion on python screener tutorial. The detailed treatment, formula, and worked examples appear in the body of this article above. The points below summarize the most important takeaways for value investors who want to apply python screener tutorial in real portfolio decisions. ValueMarkers exposes the underlying data on every covered ticker via the screener and stock profile pages, so the concepts in this article translate directly into actionable filters.
Key inputs for python screener tutorial
See the main discussion of python screener tutorial in the sections above for the full treatment, including the inputs, the calculation methodology, the typical sector benchmarks, and the most common pitfalls to avoid. The ValueMarkers screener lets value investors filter the full universe of 100,000+ stocks across 73 exchanges using python screener tutorial alongside the rest of the 120-indicator composite, with sector percentiles and historical trends shown on every stock profile.
Sector benchmarks for python screener tutorial
See the main discussion of python screener tutorial in the sections above for the full treatment, including the inputs, the calculation methodology, the typical sector benchmarks, and the most common pitfalls to avoid. The ValueMarkers screener lets value investors filter the full universe of 100,000+ stocks across 73 exchanges using python screener tutorial alongside the rest of the 120-indicator composite, with sector percentiles and historical trends shown on every stock profile.
Related ValueMarkers Resources
- Roic — Glossary entry for Roic
- Debt To Equity — Glossary entry for Debt To Equity
- Roe — Glossary entry for Roe
- Stock Screener — related ValueMarkers analysis
- Create Custom Stock Screener — related ValueMarkers analysis
- Ttd Zacks Rank — related ValueMarkers analysis
Frequently Asked Questions
what happens if the stock market crashes
When the market crashes, your Python screener becomes more active, not less. Falling prices push more stocks below your P/E threshold, and the filter returns more candidates. The key is not to adjust your filter thresholds reactively. The filters you set based on a calm, rational framework will surface quality businesses at discount prices precisely when the market is least rational about pricing them.
what time does the stock market open
U.S. markets open at 9:30 a.m. Eastern Time. For a Python screener using yfinance, run your data fetch after 5:00 p.m. Eastern to capture complete end-of-day prices. Running before close gives you intraday prices that will differ from the official close, which can cause temporary mismatches in your P/E and P/B calculations.
are stock markets closed today
U.S. markets close on 10 federal holidays. yfinance automatically returns the most recent trading day's data on holiday requests, so your script does not need holiday-handling logic. However, if you schedule your script to run at a fixed time, verify that the returned data timestamp matches your expected trading date, especially around holiday periods.
what time does the stock market close
The NYSE and Nasdaq close at 4:00 p.m. Eastern Time. If your Python screener runs before that time on a trading day, you are working with the prior session's closing prices. This is fine for fundamental filters (P/E, ROE, ROIC) since those metrics change on earnings release dates, not daily. It matters more for pure price-momentum screens where intraday price level is significant.
when does the stock market open
The U.S. market opens at 9:30 a.m. Eastern. If you are building a Python screener for global markets, schedule separate data pulls for each region's close. European exchanges close around 4:30 to 5:30 p.m. local time, Asian exchanges around 3:00 to 4:00 p.m. local time. A global screen that pulls all regions at the same UTC time will mix fresh data with stale data unless you account for session timing.
why is the stock market down today
Markets decline for many reasons: macro data releases, Fed policy shifts, geopolitical events, or sector-specific developments. When running your Python screener on a down day, check the result count. If the screen now returns 40 stocks where it returned 20 last week, the additional 20 stocks are candidates whose prices fell into your valuation range. Those are the names to investigate first: why did the price fall, and did the underlying business change?
Test on 50 tickers, validate the output against the ValueMarkers screener, and then scale to the full universe once the data checks out. The screener's 120+ indicators give you a cross-reference that makes your Python results more trustworthy from day one.
Written by Javier Sanz, Founder of ValueMarkers. Last updated April 2026.
Ready to find your next value investment?
ValueMarkers tracks 120+ fundamental indicators across 100,000+ stocks on 73 global exchanges. Run the methodology above in seconds with our stock screener, or see today's top-ranked names on the leaderboard.
Related tools: DCF Calculator · Methodology · Compare ValueMarkers
Disclaimer: This content is for informational and educational purposes only and does not constitute investment advice, a recommendation, or an offer to buy or sell any security. Past performance does not guarantee future results. Consult a licensed financial advisor before making investment decisions.