{"id":2895,"date":"2026-04-30T10:00:00","date_gmt":"2026-04-30T10:00:00","guid":{"rendered":"https:\/\/oddspapi.io\/blog\/?p=2895"},"modified":"2026-04-18T14:44:50","modified_gmt":"2026-04-18T14:44:50","slug":"line-shopping-python-best-odds","status":"publish","type":"post","link":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/","title":{"rendered":"Line Shopping in Python: Best Odds Across 350+ Books (Free API)"},"content":{"rendered":"<h2>Why Line Shopping Is the Sharpest Habit You&#8217;re Not Automating<\/h2>\n<p>Betfair Exchange pays 1.92 on Manchester City. FanDuel pays 1.83. BetRivers pays 1.83. Same game, same side, same moment. If you&#8217;re placing \u00a3100 on City and clicking the first sportsbook you see, you&#8217;re leaking 5%+ on every bet. Over a season, that&#8217;s the difference between a profitable bettor and a losing one.<\/p>\n<p>Line shopping \u2014 scanning every book you have an account with and betting at the best available price \u2014 is the single highest-EV habit in sports betting. It&#8217;s also trivial to automate. This post builds a Python CLI tool that queries 350+ bookmakers via the OddsPapi API, finds the best price for every outcome on any fixture, and flags the pricing gaps worth acting on.<\/p>\n<p>Tested on a live Premier League game (Manchester City vs Arsenal) with 131 bookmakers reporting odds. Code is under 60 lines.<\/p>\n<h2>What This Tool Is (and Isn&#8217;t)<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>This post<\/th>\n<th>Other posts<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Find the best price per outcome across every book<\/td>\n<td><a href=\"https:\/\/oddspapi.io\/blog\/arbitrage-betting-bot-python\/\">Arb bot<\/a> \u2014 scans for risk-free opportunities across the whole market<\/td>\n<\/tr>\n<tr>\n<td>CLI-style utility, readable output, no GUI<\/td>\n<td><a href=\"https:\/\/oddspapi.io\/blog\/odds-comparison-dashboard-python-streamlit\/\">Streamlit dashboard<\/a> \u2014 live web UI with charts<\/td>\n<\/tr>\n<tr>\n<td>Raw line shopping (pure price comparison)<\/td>\n<td><a href=\"https:\/\/oddspapi.io\/blog\/value-betting-scanner-python\/\">Value scanner<\/a> \u2014 finds +EV bets vs fair odds<\/td>\n<\/tr>\n<tr>\n<td>Building block: <code>best_price()<\/code> as a reusable function<\/td>\n<td><a href=\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\">Steam detector<\/a> \u2014 watches Pinnacle for line moves<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Think of line shopping as the foundation every other tool is built on. Get this right first.<\/p>\n<h2>Step 1: One API Call to Get Every Book&#8217;s Price<\/h2>\n<p>OddsPapi&#8217;s <code>\/odds<\/code> endpoint returns the current price from every bookmaker covering a fixture in a single response. Auth is a query parameter:<\/p>\n<pre class=\"wp-block-code\"><code>import requests\n\nAPI_KEY = \"YOUR_API_KEY\"\nBASE_URL = \"https:\/\/api.oddspapi.io\/v4\"\n\nfixture_id = \"id1000001761301173\"  # Man City vs Arsenal, Apr 19\n\nr = requests.get(\n    f\"{BASE_URL}\/odds\",\n    params={\"apiKey\": API_KEY, \"fixtureId\": fixture_id},\n)\ndata = r.json()\nbooks = data[\"bookmakerOdds\"]\nprint(f\"Books covering this game: {len(books)}\")\n# Books covering this game: 131\n<\/code><\/pre>\n<p>One call. 131 bookmakers. No scraping, no rate-limit juggling, no cookie farms. This is the entire premise.<\/p>\n<h2>Step 2: The <code>best_price()<\/code> Function<\/h2>\n<p>The <code>bookmakerOdds<\/code> response is deeply nested \u2014 bookmaker \u2192 market \u2192 outcome \u2192 players \u2192 price. Here&#8217;s a clean helper that walks the nesting and returns the best active price for any outcome:<\/p>\n<pre class=\"wp-block-code\"><code>def best_price(books, market_id, outcome_id):\n    \"\"\"Return (best_odds, best_book) across all active bookmakers.\"\"\"\n    best_odds, best_book = 0, None\n    for slug, b in books.items():\n        outcome = (b.get(\"markets\", {})\n                    .get(str(market_id), {})\n                    .get(\"outcomes\", {})\n                    .get(str(outcome_id), {}))\n        player = outcome.get(\"players\", {}).get(\"0\", {})\n        # Skip inactive outcomes \u2014 they ship with stale prices\n        if not player.get(\"active\"):\n            continue\n        price = player.get(\"price\")\n        if price and price &gt; best_odds:\n            best_odds, best_book = price, slug\n    return best_odds, best_book\n<\/code><\/pre>\n<p>Three defensive details matter:<\/p>\n<ul>\n<li><strong>Always check <code>active<\/code>.<\/strong> Suspended markets still ship a <code>price<\/code> field \u2014 it&#8217;s just stale. Filter them out or you&#8217;ll shop for lines that no one will honor.<\/li>\n<li><strong>Always use <code>.get()<\/code> chains.<\/strong> Not every book carries every market. Expect missing keys.<\/li>\n<li><strong>Cast market IDs to strings.<\/strong> They come back as strings in the response (<code>\"101\"<\/code>, not <code>101<\/code>).<\/li>\n<\/ul>\n<h2>Step 3: Shop the Match Result (1X2)<\/h2>\n<p>Soccer 1X2 lives at market <code>101<\/code>. Outcomes are 101=Home, 102=Draw, 103=Away:<\/p>\n<pre class=\"wp-block-code\"><code>OUTCOMES = {\n    101: \"Home\",\n    102: \"Draw\",\n    103: \"Away\",\n}\n\nprint(f\"\\n{'Outcome':&lt;8} {'Best':&lt;7} {'Book':&lt;18}\")\nfor outcome_id, name in OUTCOMES.items():\n    price, book = best_price(books, 101, outcome_id)\n    print(f\"{name:&lt;8} {price:&lt;7} {book}\")\n<\/code><\/pre>\n<p>Live result for Man City vs Arsenal (131 books, filtered to active outcomes):<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Outcome<\/th>\n<th>Best Price<\/th>\n<th>Book<\/th>\n<th>Average<\/th>\n<th>Gap vs Avg<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Home (City)<\/td>\n<td>2.20<\/td>\n<td>paddypower<\/td>\n<td>1.855<\/td>\n<td>+18.6%<\/td>\n<\/tr>\n<tr>\n<td>Draw<\/td>\n<td>4.348<\/td>\n<td>kalshi<\/td>\n<td>3.586<\/td>\n<td>+21.2%<\/td>\n<\/tr>\n<tr>\n<td>Away (Arsenal)<\/td>\n<td>4.60<\/td>\n<td>unibet.nl<\/td>\n<td>4.220<\/td>\n<td>+9.0%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>The worst price at each outcome was ~1.56 on City (<code>novig.us<\/code>), ~3.20 on the Draw (<code>pokerstars.fr<\/code>), and ~2.80 on Arsenal (<code>pokerstars.fr<\/code>). Betting Arsenal at 2.80 instead of 4.60 means giving up <strong>64% of your potential profit<\/strong> on a winning ticket.<\/p>\n<h2>Step 4: The &#8220;Book Sum&#8221; Sanity Check<\/h2>\n<p>Add the implied probabilities of the best prices. A fair book with no margin would sum to 1.00. Real bookmakers build in a 4\u20138% margin, so book sums of 1.04\u20131.08 are normal. If your sum is below 1.00, you&#8217;ve spotted an arbitrage. If it&#8217;s far below 1.00 (like 0.90), one of your prices is probably bogus:<\/p>\n<pre class=\"wp-block-code\"><code>best_h, _ = best_price(books, 101, 101)\nbest_d, _ = best_price(books, 101, 102)\nbest_a, _ = best_price(books, 101, 103)\n\nbook_sum = 1\/best_h + 1\/best_d + 1\/best_a\nmargin = (book_sum - 1) * 100\nprint(f\"Best prices: {best_h} \/ {best_d} \/ {best_a}\")\nprint(f\"Book sum: {book_sum:.4f}  ({margin:+.2f}% margin)\")\n\n# Output:\n# Best prices: 2.2 \/ 4.348 \/ 4.6\n# Book sum: 0.9019  (-9.81% margin)\n<\/code><\/pre>\n<p>A 9.8% arb is too good to be true. In practice, one of three things is happening:<\/p>\n<ul>\n<li><strong>Enhanced\/boosted price:<\/strong> <code>paddypower<\/code>&#8216;s 2.2 on Man City is likely a price-boost promo. Limits will be tiny, and the TOS often bans arbing these.<\/li>\n<li><strong>Regional quirk:<\/strong> <code>unibet.nl<\/code> and <code>kalshi<\/code> are pricing for a different equilibrium than mainstream UK\/US books.<\/li>\n<li><strong>Stale line:<\/strong> a book hasn&#8217;t updated since the line moved.<\/li>\n<\/ul>\n<p>The tool does its job \u2014 it surfaces the outliers. You decide whether to trust them.<\/p>\n<h2>Step 5: Filter to Books You Actually Bet With<\/h2>\n<p>Line shopping is only useful if you have money at each book. Filter your scan to a whitelist:<\/p>\n<pre class=\"wp-block-code\"><code>MY_BOOKS = {\n    \"pinnacle\", \"bet365\", \"draftkings\", \"fanduel\",\n    \"betmgm\", \"caesars\", \"betrivers\", \"betfair-ex\",\n}\n\ndef best_price_filtered(books, market_id, outcome_id, whitelist):\n    filtered = {slug: b for slug, b in books.items() if slug in whitelist}\n    return best_price(filtered, market_id, outcome_id)\n\nfor outcome_id, name in OUTCOMES.items():\n    price, book = best_price_filtered(books, 101, outcome_id, MY_BOOKS)\n    print(f\"{name:&lt;8} {price:&lt;7} {book}\")\n<\/code><\/pre>\n<p>Filtered to sharp-trio + major US books, the spread shrinks and the prices are all actually takeable:<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Outcome<\/th>\n<th>Best<\/th>\n<th>Book<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Home (City)<\/td>\n<td>1.92<\/td>\n<td>betfair-ex<\/td>\n<\/tr>\n<tr>\n<td>Draw<\/td>\n<td>3.80<\/td>\n<td>betfair-ex<\/td>\n<\/tr>\n<tr>\n<td>Away (Arsenal)<\/td>\n<td>4.50<\/td>\n<td>betfair-ex<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Book sum = 1\/1.92 + 1\/3.80 + 1\/4.50 = <strong>1.006<\/strong> \u2192 0.6% margin. That&#8217;s tight \u2014 the exchange is pricing near-fair across all three sides. For this fixture, the exchange beats every US sportsbook on every outcome. Whether you take it depends on commission \u2014 Betfair charges 2\u20135% on winnings, which eats most of the edge vs Pinnacle (1.877 \/ 3.65 \/ 4.37).<\/p>\n<h2>Step 6: Expand to Over\/Under Totals<\/h2>\n<p>The same function works for any two-way market. Over\/Under 2.5 Goals in soccer lives at market <code>1010<\/code>:<\/p>\n<pre class=\"wp-block-code\"><code>TOTALS_25 = {\n    1010: \"Over 2.5\",\n    1011: \"Under 2.5\",\n}\n\nprint(f\"\\nOver\/Under 2.5 Goals:\")\nfor outcome_id, name in TOTALS_25.items():\n    price, book = best_price(books, 1010, outcome_id)\n    print(f\"  {name:&lt;10} {price:&lt;7} {book}\")\n\n# Output (114 books carry this market):\n# Over 2.5   2.07    betfair-ex\n# Under 2.5  1.92    betfair-ex\n<\/code><\/pre>\n<p>Betfair Exchange wins both sides here \u2014 exchange pricing tends to be tighter than sportsbook pricing because there&#8217;s no bookmaker margin, just a commission on winnings.<\/p>\n<h2>Step 7: Put It Together \u2014 A Real CLI Shopper<\/h2>\n<p>Wire everything into a single script you can run from the terminal:<\/p>\n<pre class=\"wp-block-code\"><code>import sys\nimport requests\n\nAPI_KEY = \"YOUR_API_KEY\"\nBASE = \"https:\/\/api.oddspapi.io\/v4\"\n\n# Market IDs for soccer. Look up more via \/v4\/markets?sportId=10\nMARKETS = {\n    101:  (\"Match Result (1X2)\", {101: \"Home\", 102: \"Draw\", 103: \"Away\"}),\n    1010: (\"Over\/Under 2.5 Goals\", {1010: \"Over 2.5\", 1011: \"Under 2.5\"}),\n    104:  (\"Both Teams to Score\", {104: \"Yes\", 105: \"No\"}),\n}\n\ndef best_price(books, market_id, outcome_id):\n    best_odds, best_book = 0, None\n    for slug, b in books.items():\n        player = (b.get(\"markets\", {})\n                   .get(str(market_id), {})\n                   .get(\"outcomes\", {})\n                   .get(str(outcome_id), {})\n                   .get(\"players\", {})\n                   .get(\"0\", {}))\n        if not player.get(\"active\"):\n            continue\n        price = player.get(\"price\")\n        if price and price &gt; best_odds:\n            best_odds, best_book = price, slug\n    return best_odds, best_book\n\ndef shop(fixture_id):\n    r = requests.get(f\"{BASE}\/odds\",\n                     params={\"apiKey\": API_KEY, \"fixtureId\": fixture_id})\n    r.raise_for_status()\n    books = r.json().get(\"bookmakerOdds\", {})\n    print(f\"Books covering fixture: {len(books)}\\n\")\n\n    for market_id, (market_name, outcomes) in MARKETS.items():\n        print(f\"=== {market_name} ===\")\n        for outcome_id, name in outcomes.items():\n            price, book = best_price(books, market_id, outcome_id)\n            if price:\n                print(f\"  {name:&lt;12} {price:&lt;7} @ {book}\")\n        print()\n\nif __name__ == \"__main__\":\n    shop(sys.argv[1])\n<\/code><\/pre>\n<p>Run it:<\/p>\n<pre class=\"wp-block-code\"><code>$ python line_shopper.py id1000001761301173\nBooks covering fixture: 131\n\n=== Match Result (1X2) ===\n  Home         2.2     @ paddypower\n  Draw         4.348   @ kalshi\n  Away         4.6     @ unibet.nl\n\n=== Over\/Under 2.5 Goals ===\n  Over 2.5     2.07    @ betfair-ex\n  Under 2.5    1.92    @ betfair-ex\n\n=== Both Teams to Score ===\n  Yes          2.0     @ betfair-ex\n  No           2.0     @ betfair-ex\n<\/code><\/pre>\n<p>Sixty lines of Python and you now have a scanner that outperforms 95% of retail bettors&#8217; workflow.<\/p>\n<h2>Step 8: Loop Across Every Fixture Today<\/h2>\n<p>Line shopping one game is a party trick. Line shopping every game on the slate, sorted by biggest pricing gap, is a business. Here&#8217;s the outer loop:<\/p>\n<pre class=\"wp-block-code\"><code>import time\nfrom datetime import date, timedelta\n\ntoday = date.today().isoformat()\ntomorrow = (date.today() + timedelta(days=1)).isoformat()\n\n# Get all soccer fixtures for the next 24 hours\nr = requests.get(f\"{BASE}\/fixtures\", params={\n    \"apiKey\": API_KEY, \"sportId\": 10,\n    \"from\": today, \"to\": tomorrow,\n})\nfixtures = [f for f in r.json() if f.get(\"hasOdds\")]\n\ngaps = []\nfor f in fixtures:\n    time.sleep(0.2)  # ~0.88s cooldown per endpoint \u2014 be polite\n    r2 = requests.get(f\"{BASE}\/odds\",\n                      params={\"apiKey\": API_KEY, \"fixtureId\": f[\"fixtureId\"]})\n    books = r2.json().get(\"bookmakerOdds\", {})\n    if len(books) &lt; 20:\n        continue\n\n    # Find the biggest gap on the favorite (outcome 101 or 103)\n    for side_oid in (101, 103):\n        best_odds, best_book = best_price(books, 101, side_oid)\n        worst_odds = 999\n        for slug, b in books.items():\n            p = (b.get(\"markets\", {}).get(\"101\", {})\n                  .get(\"outcomes\", {}).get(str(side_oid), {})\n                  .get(\"players\", {}).get(\"0\", {}))\n            if p.get(\"active\") and p.get(\"price\") and p[\"price\"] &lt; worst_odds:\n                worst_odds = p[\"price\"]\n        if worst_odds &lt; 999:\n            gap_pct = (best_odds \/ worst_odds - 1) * 100\n            gaps.append((gap_pct, f, side_oid, best_odds, best_book))\n\n# Biggest line-shopping edges on today's slate\ngaps.sort(reverse=True)\nfor gap, f, oid, best, book in gaps[:10]:\n    teams = f\"{f['participant1Name']} vs {f['participant2Name']}\"\n    side = \"Home\" if oid == 101 else \"Away\"\n    print(f\"{gap:6.1f}%  {teams:&lt;50}  {side}  {best} @ {book}\")\n<\/code><\/pre>\n<p>A 30\u201350% gap on a major-league fixture is noise (limits, promos, regional quirks). A consistent 5\u201310% gap across games at a specific book tells you which book to be targeting routinely.<\/p>\n<h2>What Market IDs Do I Look Up?<\/h2>\n<p>Every sport has its own market catalog. Look them up instead of hardcoding:<\/p>\n<pre class=\"wp-block-code\"><code>r = requests.get(f\"{BASE}\/markets\", params={\"apiKey\": API_KEY, \"sportId\": 10})\ncat = [m for m in r.json() if m[\"sportId\"] == 10]\nprint(f\"Soccer markets: {len(cat)}\")\n\n# Build a name lookup\nlookup = {m[\"marketId\"]: m[\"marketName\"] for m in cat if m[\"handicap\"] == 0}\nprint(lookup[101])   # 'Full Time Result'\nprint(lookup[104])   # 'Both Teams To Score'\n<\/code><\/pre>\n<p>Common market IDs worth hardcoding:<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Sport<\/th>\n<th>Market<\/th>\n<th>Market ID<\/th>\n<th>Outcome IDs<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Soccer<\/td>\n<td>Match Result (1X2)<\/td>\n<td>101<\/td>\n<td>101\/102\/103<\/td>\n<\/tr>\n<tr>\n<td>Soccer<\/td>\n<td>Over\/Under 2.5 Goals<\/td>\n<td>1010<\/td>\n<td>1010\/1011<\/td>\n<\/tr>\n<tr>\n<td>Soccer<\/td>\n<td>Both Teams To Score<\/td>\n<td>104<\/td>\n<td>104\/105<\/td>\n<\/tr>\n<tr>\n<td>NBA<\/td>\n<td>Moneyline<\/td>\n<td>111<\/td>\n<td>111\/112<\/td>\n<\/tr>\n<tr>\n<td>NHL<\/td>\n<td>Moneyline (incl. OT)<\/td>\n<td>151<\/td>\n<td>151\/152<\/td>\n<\/tr>\n<tr>\n<td>MLB<\/td>\n<td>Moneyline<\/td>\n<td>131<\/td>\n<td>131\/132<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>Production Notes<\/h2>\n<ul>\n<li><strong>Rate limits:<\/strong> ~0.88s cooldown per endpoint on the free tier. Loop with <code>time.sleep(0.2)<\/code> for safety.<\/li>\n<li><strong>WebSocket:<\/strong> polling 100 fixtures every minute is fine, but for true real-time shopping use the WebSocket feed \u2014 prices push as they change. See the <a href=\"https:\/\/oddspapi.io\/blog\/websocket-odds-api-real-time-betting-data\/\">WebSocket tutorial<\/a>.<\/li>\n<li><strong>Exchange odds:<\/strong> Betfair Exchange, Polymarket, SX Bet, ProphetX all return back-prices in the main <code>price<\/code> field. If you want lay-side access, check <code>exchangeMeta<\/code>.<\/li>\n<li><strong>Regional clones:<\/strong> some bookmakers appear as multiple slugs (<code>betano<\/code>, <code>betano.bet.br<\/code>, <code>betano.co.uk<\/code>). The <code>\/v4\/bookmakers<\/code> endpoint has a <code>cloneOf<\/code> field \u2014 dedupe if you&#8217;re treating &#8220;best price&#8221; as &#8220;best unique book.&#8221;<\/li>\n<\/ul>\n<h2>Stop Clicking Five Tabs. Start Shopping.<\/h2>\n<p>Line shopping isn&#8217;t glamorous. It&#8217;s not a model, it&#8217;s not a strategy, it&#8217;s not a system. It&#8217;s a 60-line script that runs in 2 seconds and guarantees you never leave money on the table. Every +EV bettor runs something like this. Most retail bettors don&#8217;t.<\/p>\n<p><a href=\"https:\/\/oddspapi.io\"><strong>Grab a free OddsPapi API key<\/strong><\/a> and you&#8217;re running this scanner on today&#8217;s slate in the next ten minutes.<\/p>\n<h2>FAQ<\/h2>\n<h3>What&#8217;s the difference between line shopping and arbitrage?<\/h3>\n<p>Line shopping is finding the single best price for one outcome. Arbitrage is finding a combination of prices across outcomes that guarantees profit regardless of result. Line shopping is a prerequisite for arbitrage \u2014 you can&#8217;t arb without knowing which book has the best price on each side.<\/p>\n<h3>How many bookmakers should I actually compare?<\/h3>\n<p>As many as you have accounts with. Realistic numbers: US sharp bettors typically hold 5\u201310 books (DraftKings, FanDuel, BetMGM, Caesars, BetRivers, Pinnacle via proxy, Betfair Exchange). European bettors often hold 15\u201320. OddsPapi covers all of them on one endpoint.<\/p>\n<h3>Why are some &#8220;best prices&#8221; obviously wrong?<\/h3>\n<p>Enhanced price boosts, stale lines on suspended markets, regional Unibet\/Bet365\/bwin variants with different pricing, and prediction-market venues (Kalshi, Polymarket) all show up in raw scans. Always filter to <code>active: True<\/code> outcomes and a whitelist of books you actually use.<\/p>\n<h3>Is historical line-shopping data free?<\/h3>\n<p>Yes. OddsPapi&#8217;s <code>\/historical-odds<\/code> endpoint is on the free tier. Use it to backtest a line-shopping strategy \u2014 find every game in the last 12 months where your best-price combo beat the closing line by 3%+ and measure your edge.<\/p>\n<h3>Can I run this in real time?<\/h3>\n<p>Polling works for most use cases. For true tick-level shopping (arb scanners, latency-sensitive strategies), subscribe to the WebSocket feed and push every price change directly into your best-price table.<\/p>\n<p><!--\nFocus Keyphrase: line shopping python\nSEO Title: Line Shopping in Python: Best Odds Across 350+ Books (Free API)\nMeta Description: Build a Python line-shopping tool that scans 350+ bookmakers for the best price on every outcome. 60 lines of code, tested on real EPL data.\nSlug: line-shopping-python-best-odds\n--><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Build a Python line-shopping tool that scans 350+ bookmakers for the best price on every outcome. 60 lines of code, tested on real EPL data.<\/p>\n","protected":false},"author":2,"featured_media":2896,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[8,59,9,11,10],"class_list":["post-2895","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to-guides","tag-free-api","tag-line-shopping","tag-odds-api","tag-python","tag-sports-betting-api"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Line Shopping in Python: Best Odds Across 350+ Books (Free API) | Odds API Development Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Line Shopping in Python: Best Odds Across 350+ Books (Free API) | Odds API Development Blog\" \/>\n<meta property=\"og:description\" content=\"Build a Python line-shopping tool that scans 350+ bookmakers for the best price on every outcome. 60 lines of code, tested on real EPL data.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\" \/>\n<meta property=\"og:site_name\" content=\"Odds API Development Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-04-30T10:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp\" \/>\n\t<meta property=\"og:image:width\" content=\"2560\" \/>\n\t<meta property=\"og:image:height\" content=\"1429\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/webp\" \/>\n<meta name=\"author\" content=\"Odds API Writer\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/oddspapi.io\/logo-v2.webp\" \/>\n<meta name=\"twitter:creator\" content=\"@oddspapiapi\" \/>\n<meta name=\"twitter:site\" content=\"@oddspapiapi\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Odds API Writer\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\"},\"author\":{\"name\":\"Odds API Writer\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\"},\"headline\":\"Line Shopping in Python: Best Odds Across 350+ Books (Free API)\",\"datePublished\":\"2026-04-30T10:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\"},\"wordCount\":1276,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp\",\"keywords\":[\"Free API\",\"Line Shopping\",\"Odds API\",\"Python\",\"Sports Betting API\"],\"articleSection\":[\"How To Guides\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\",\"name\":\"Line Shopping in Python: Best Odds Across 350+ Books (Free API) | Odds API Development Blog\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp\",\"datePublished\":\"2026-04-30T10:00:00+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#primaryimage\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp\",\"width\":2560,\"height\":1429,\"caption\":\"Line Shopping - OddsPapi API Blog\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/oddspapi.io\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Line Shopping in Python: Best Odds Across 350+ Books (Free API)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"name\":\"OddsPapi\",\"description\":\"Sports Odds APIs Tutorials &amp; Guides\",\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"alternateName\":\"Odds Papi\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/oddspapi.io\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\",\"name\":\"OddsPapi\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png\",\"width\":135,\"height\":135,\"caption\":\"OddsPapi\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/x.com\/oddspapiapi\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\",\"name\":\"Odds API Writer\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g\",\"caption\":\"Odds API Writer\"},\"url\":\"https:\/\/oddspapi.io\/blog\/author\/andy-lavelle\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Line Shopping in Python: Best Odds Across 350+ Books (Free API) | Odds API Development Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/","og_locale":"en_US","og_type":"article","og_title":"Line Shopping in Python: Best Odds Across 350+ Books (Free API) | Odds API Development Blog","og_description":"Build a Python line-shopping tool that scans 350+ bookmakers for the best price on every outcome. 60 lines of code, tested on real EPL data.","og_url":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/","og_site_name":"Odds API Development Blog","article_published_time":"2026-04-30T10:00:00+00:00","og_image":[{"width":2560,"height":1429,"url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp","type":"image\/webp"}],"author":"Odds API Writer","twitter_card":"summary_large_image","twitter_image":"https:\/\/oddspapi.io\/logo-v2.webp","twitter_creator":"@oddspapiapi","twitter_site":"@oddspapiapi","twitter_misc":{"Written by":"Odds API Writer","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#article","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/"},"author":{"name":"Odds API Writer","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13"},"headline":"Line Shopping in Python: Best Odds Across 350+ Books (Free API)","datePublished":"2026-04-30T10:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/"},"wordCount":1276,"commentCount":0,"publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp","keywords":["Free API","Line Shopping","Odds API","Python","Sports Betting API"],"articleSection":["How To Guides"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/","url":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/","name":"Line Shopping in Python: Best Odds Across 350+ Books (Free API) | Odds API Development Blog","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#primaryimage"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp","datePublished":"2026-04-30T10:00:00+00:00","breadcrumb":{"@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#primaryimage","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/line-shopping-python-best-odds-scaled.webp","width":2560,"height":1429,"caption":"Line Shopping - OddsPapi API Blog"},{"@type":"BreadcrumbList","@id":"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/oddspapi.io\/blog\/"},{"@type":"ListItem","position":2,"name":"Line Shopping in Python: Best Odds Across 350+ Books (Free API)"}]},{"@type":"WebSite","@id":"https:\/\/oddspapi.io\/blog\/#website","url":"https:\/\/oddspapi.io\/blog\/","name":"OddsPapi","description":"Sports Odds APIs Tutorials &amp; Guides","publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"alternateName":"Odds Papi","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/oddspapi.io\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/oddspapi.io\/blog\/#organization","name":"OddsPapi","url":"https:\/\/oddspapi.io\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png","width":135,"height":135,"caption":"OddsPapi"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/oddspapiapi"]},{"@type":"Person","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13","name":"Odds API Writer","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g","caption":"Odds API Writer"},"url":"https:\/\/oddspapi.io\/blog\/author\/andy-lavelle\/"}]}},"_links":{"self":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2895","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/comments?post=2895"}],"version-history":[{"count":1,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2895\/revisions"}],"predecessor-version":[{"id":2897,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2895\/revisions\/2897"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media\/2896"}],"wp:attachment":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media?parent=2895"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/categories?post=2895"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/tags?post=2895"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}