DEV Community

Cover image for Fetching Live Football Scores and Odds with Python: A Beginner's Guide (2026)
orbistats
orbistats

Posted on

Fetching Live Football Scores and Odds with Python: A Beginner's Guide (2026)

If you have ever wanted to build a Discord bot that shouts when a goal is scored, a Telegram alert for kickoff, or a small dashboard that shows live scores next to bookmaker odds, the first thing you need is a reliable data feed. The good news is that in 2026 you do not need to scrape websites or fight with broken HTML. A proper sports data API gives you clean JSON, and Python's requests library is all you need to start using it.

In this guide we will build a small but real Python project from scratch. We will fetch today's live football scores, pull odds for the same matches, convert those odds into implied probabilities, and wrap everything in a polling loop that respects rate limits. We will use the Orbistats sports data API as the data source, because it has a free tier with no card required, but the patterns here work with almost any REST sports API.

What You Will Build

By the end of this tutorial you will have a single Python script that does the following. It loads your API key safely from an environment file, calls the API with proper authentication, handles errors and timeouts without crashing, prints live football scores in a clean format, fetches odds and turns them into percentages you can actually reason about, and polls on a sensible interval so you never burn through your daily quota.

This is a beginner-friendly guide, so I will explain why each piece exists instead of just dropping code. If you already know Python, you can skim the explanations and copy the snippets.

Prerequisites

You need Python 3.10 or newer, a terminal, and a free API key. You can create a key on the Orbistats site without entering a card, and the free tier gives you 150 requests per day across all endpoints, which is plenty for learning and for small personal projects.

If you would rather see what real responses look like before writing any code, there is a public API sandbox where you can try requests straight from the browser. I recommend doing this once, because knowing the shape of the JSON makes the Python code much easier to follow.

Step 1: Set Up Your Project

Create a folder, a virtual environment, and install the two packages we need.

bash
mkdir football-live && cd football-live
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests python-dotenv

A virtual environment keeps this project's packages separate from everything else on your machine, which saves you from version conflicts later. requests handles the HTTP calls and python-dotenv loads secrets from a file so they never end up in your code.

Now create a file called .env in the project folder:

ORBISTATS_API_KEY=your_key_here

And add it to .gitignore immediately so you never commit it by accident:

bash
echo ".env" >> .gitignore
echo ".venv/" >> .gitignore

This is the single most common beginner mistake with APIs. A key pushed to a public GitHub repo can be found by bots within minutes, so build the habit on day one.

Step 2: Build a Small API Client

Instead of calling requests.get all over the place, we will put the shared logic in one function. That gives us one place for authentication, timeouts, and error handling.

python
import os
import time
import requests
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("ORBISTATS_API_KEY")
BASE_URL = "https://api.orbistats.com/v1"

if not API_KEY:
raise SystemExit("Missing ORBISTATS_API_KEY. Add it to your .env file.")

session = requests.Session()
session.headers.update({"Authorization": f"Bearer {API_KEY}"})

def api_get(path, params=None, retries=3):
url = f"{BASE_URL}{path}"
for attempt in range(1, retries + 1):
try:
res = session.get(url, params=params, timeout=10)
except requests.RequestException as exc:
print(f"Network error ({exc}), attempt {attempt}/{retries}")
time.sleep(2 ** attempt)
continue

    if res.status_code == 429:
        wait = int(res.headers.get("Retry-After", 5))
        print(f"Rate limited. Waiting {wait}s")
        time.sleep(wait)
        continue

    if not res.ok:
        raise RuntimeError(f"API error {res.status_code}: {res.text[:200]}")

    return res.json()

raise RuntimeError("API request failed after retries")
Enter fullscreen mode Exit fullscreen mode

There are a few things worth understanding here. Using a Session reuses the underlying connection, which is faster when you make many calls. The timeout=10 matters because without it a stalled request can hang your script forever. The retry loop uses exponential backoff, meaning it waits 2 seconds, then 4, then 8, so a temporary network hiccup does not kill your program. And the 429 branch is important: that status code means you are sending requests too fast, and the polite response is to wait and try again.

Step 3: Fetch Live Football Scores

Now the fun part. The exact field names depend on the response schema, so check the sandbox and adjust the keys if needed. The overall pattern stays the same.

python
def get_live_scores():
payload = api_get("/football/live")
return payload.get("data", [])

def print_scores(matches):
if not matches:
print("No live matches right now.")
return

for m in matches:
    home = m.get("home_team")
    away = m.get("away_team")
    hs = m.get("home_score", 0)
    as_ = m.get("away_score", 0)
    minute = m.get("minute", "?")
    print(f"{home} {hs} - {as_} {away}  ({minute}')")
Enter fullscreen mode Exit fullscreen mode

if name == "main":
print_scores(get_live_scores())

Run it with python main.py. If a match is live, you will see lines like Arsenal 2 - 1 Chelsea (67'). If nothing is on, you will get the friendly empty message instead of a crash, which is exactly what you want at 4 a.m. when there is no football.

Notice the use of .get() instead of square brackets. Real-world APIs sometimes omit fields, and .get() returns None instead of raising a KeyError. Defensive access like this is what separates a demo from something you can leave running.

Step 4: Fetch Fixtures for Today

Live scores only cover matches in progress. For a fuller picture, you also want today's fixtures, including games that have not started yet.

python
def get_fixtures(date="today"):
payload = api_get("/football/fixtures", params={"date": date})
return payload.get("data", [])

This uses the same /football/fixtures?date=today pattern shown in other Orbistats tutorials, such as this vanilla JavaScript live scoreboard. If you prefer JavaScript for the front end, that guide is a nice companion to this backend-focused one.

Step 5: Add Odds

Scores tell you what is happening. Odds tell you what the market thinks will happen. Here is a helper that fetches odds for a given fixture.

python
def get_odds(fixture_id):
payload = api_get("/football/odds", params={"fixture_id": fixture_id})
return payload.get("data", [])

Odds usually arrive in decimal format, for example 2.50 for the home team, 3.20 for a draw, and 2.90 for the away team. Decimal odds show how much you would receive per unit staked, including the stake itself. They are easy to work with, but they are not very intuitive, so let us convert them into probabilities.

Step 6: Convert Odds to Implied Probability

The implied probability of decimal odds is simply 1 / odds. But there is a catch that most beginners miss: bookmakers build a margin into their prices, so the raw implied probabilities of all outcomes add up to more than 100%. That excess is called the overround. To get fair-looking numbers, you normalize.

python
def implied_probabilities(home, draw, away):
raw = [1 / home, 1 / draw, 1 / away]
total = sum(raw) # greater than 1 because of the bookmaker margin
fair = [round(p / total * 100, 1) for p in raw]
margin = round((total - 1) * 100, 2)
return {"home": fair[0], "draw": fair[1], "away": fair[2], "margin": margin}

print(implied_probabilities(2.50, 3.20, 2.90))

{'home': 38.4, 'draw': 30.0, 'away': 33.1, 'margin': 4.17}

With those example odds, the market roughly gives the home side a 38% chance, a draw 30%, and the away side 33%, with a bookmaker margin of about 4%. Once you can do this conversion, you can compare bookmakers, spot unusual price gaps, or display "win chance" bars in a dashboard instead of confusing decimals. This is for information and analysis only, and nothing here is betting advice.

Step 7: Combine Scores and Odds

Now we join the two datasets so each live match is printed with its market view.

python
def show_live_with_odds():
matches = get_live_scores()
if not matches:
print("No live matches right now.")
return

for m in matches:
    line = f"{m['home_team']} {m.get('home_score', 0)} - {m.get('away_score', 0)} {m['away_team']}"
    try:
        odds = get_odds(m["id"])
        if odds:
            o = odds[0]
            probs = implied_probabilities(o["home"], o["draw"], o["away"])
            line += f"  | Win chance: H {probs['home']}% D {probs['draw']}% A {probs['away']}%"
    except (RuntimeError, KeyError):
        line += "  | odds unavailable"
    print(line)
Enter fullscreen mode Exit fullscreen mode

The try/except is deliberate. If one match has no odds, we do not want the whole loop to fail, so we print a fallback and keep going.

Step 8: Poll Without Burning Your Quota

This is where beginners get stuck. On a 150-requests-per-day plan, polling every 10 seconds would use up your quota in about 25 minutes. The fix is to be smart about when and how often you poll.

python
POLL_SECONDS = 60

def run():
while True:
try:
matches = get_live_scores()
except RuntimeError as exc:
print(f"Skipping this cycle: {exc}")
time.sleep(POLL_SECONDS)
continue

    if matches:
        print_scores(matches)
        time.sleep(POLL_SECONDS)
    else:
        # Nothing live, so back off and check far less often
        print("Nothing live. Sleeping 15 minutes.")
        time.sleep(15 * 60)
Enter fullscreen mode Exit fullscreen mode

if name == "main":
run()

The idea is simple: poll faster while games are live, and slow down dramatically when nothing is happening. A few more habits help a lot. Cache responses for a short time so repeated calls do not hit the API. Fetch fixtures once in the morning instead of every minute. And only request odds for matches you actually care about.

If you need updates the instant a goal is scored, polling is the wrong tool. A WebSocket connection pushes changes to you instead, and this walkthrough on building a live scoreboard with a WebSocket API explains the REST-plus-WebSocket pattern in detail, including reconnection and resyncing.

Step 9: Wrap It in a Tiny FastAPI Service (Optional)

Once your script works, you might want to expose the data to other apps, such as a website or a mobile client, without leaking your API key. A small FastAPI wrapper lets you do exactly that, and also gives you a place to cache.

python
from fastapi import FastAPI

app = FastAPI()

@app.get("/live")
def live():
return {"matches": get_live_scores()}

Run it with uvicorn main:app --reload and your own /live endpoint is ready. For a fuller version with caching and your own auth layer, read the Python Requests and FastAPI example from the same series.

Beyond Football: Same Code, 13 Sports

One of the nicest things about a consistent API is that your code barely changes when you move to a new sport. Orbistats now covers 13 sports, and the response shape follows the same pattern across them, so switching usually means changing a single path segment.

python
def get_live(sport="football"):
return api_get(f"/{sport}/live").get("data", [])

for sport in ["football", "basketball", "tennis"]:
print(sport, len(get_live(sport)))

Check the current sport list and exact path names in the developer documentation, since they can change as new sports are added.

Common Mistakes to Avoid

Hardcoding the API key in your script is the classic one, so always use environment variables. Skipping timeouts is another, because a hung request can freeze your bot silently. Ignoring the 429 status leads to bans or wasted retries. Trusting every field to exist causes KeyError crashes at the worst moments. And polling at a fixed fast interval, whether or not matches are live, burns through your daily quota for no benefit.

Where to Go Next

You now have a working Python foundation for live football data. From here you can send goal alerts to Discord or Telegram, store results in SQLite for your own history, build a dashboard with Streamlit, or compare odds movement over time. If Python is not your only language, the same API is covered in other guides: C# with .NET Core, and Go with net/http. You can follow more tutorials on the Orbistats DEV profile.

Ready to build your own version? Grab a free key from the pricing page, test a few calls in the sandbox, and try adding a second sport today. If you build something with it, share it in the comments. I would love to see what you make.

Top comments (0)