Odds Converter in Python: Decimal, American, Fractional & Polymarket Shares

Odds Converter in Python - OddsPapi API Blog
How To Guides July 1, 2026

Every odds tool eventually hits the same wall: the data comes in one format and your model wants another. Pinnacle ships decimal, US books quote American, the UK still prints fractional, and prediction markets like Polymarket price everything as a share price between 0 and 1. Before you can compare a single line, you have to normalise all of it.

This guide gives you a tested, dependency-free Python module that converts between decimal, American, fractional, implied probability, and Polymarket share prices in both directions. Then it shows you the shortcut: a free odds API that already ships every format for you, so the only converter you really need is the one for prediction-market shares.

Why Hand-Rolled Converters Bite You

The math looks trivial until you hit the edge cases that quietly corrupt a betting model:

  • The +100 / 2.00 boundary. American odds use a different formula above and below even money. Get the branch wrong and every favourite in your dataset is mispriced.
  • Fractional rounding. 1.493 decimal is not exactly 35/71, it is an approximation. Naive code prints 493/1000 and looks amateur.
  • Share-price inversion. Polymarket and Kalshi quote a 0–1 probability, not odds. A YES share at 0.67 is decimal 1.493, but only if you remember to invert and not just multiply by 100.
  • Vig contamination. Implied probability straight off a single book includes the bookmaker margin. Treat it as a true probability and your expected-value math is wrong from the first line.

None of this is hard. It is just easy to get subtly wrong, and a subtle error in a converter poisons everything downstream. Below is a module that handles all of it, validated against live data from the OddsPapi feed.

The Old Way vs The OddsPapi Way

Task Old Way (DIY) OddsPapi
Decimal odds Write + test the formula price field, ready to use
American odds Branch on the 2.00 boundary priceAmerican string
Fractional odds Continued-fraction approximation priceFractional string
Polymarket share price Scrape the CLOB, invert yourself price already in decimal odds
Coverage One book at a time 350+ books, sharps + exchanges

OddsPapi pre-converts every price into decimal, American, and fractional on the way out of the feed. The headline trick: it also normalises Polymarket and Kalshi share prices into decimal odds for you, which is the one conversion most developers get wrong. More on that below.

The Converter Module (Pure Python, No Dependencies)

Drop this into odds_convert.py. It uses only the standard library (fractions for clean fractional output). Every function round-trips with the others.

from fractions import Fraction


def decimal_to_american(d: float) -> str:
    """Decimal odds -> American moneyline string (e.g. '-203', '+186')."""
    if d <= 1:
        raise ValueError("decimal odds must be > 1")
    if d >= 2:
        return f"+{round((d - 1) * 100)}"
    return f"{round(-100 / (d - 1))}"


def american_to_decimal(a) -> float:
    """American moneyline -> decimal odds."""
    a = int(a)
    if a > 0:
        return round(1 + a / 100, 4)
    return round(1 + 100 / abs(a), 4)


def decimal_to_fractional(d: float, max_den: int = 100) -> str:
    """Decimal odds -> reduced fractional string (e.g. '13/7')."""
    frac = Fraction(d - 1).limit_denominator(max_den)
    return f"{frac.numerator}/{frac.denominator}"


def fractional_to_decimal(frac: str) -> float:
    """Fractional string -> decimal odds."""
    num, den = frac.split("/")
    return round(1 + int(num) / int(den), 4)


def decimal_to_probability(d: float) -> float:
    """Decimal odds -> implied probability (%). NOTE: includes the vig."""
    return round(100 / d, 2)


def probability_to_decimal(p: float) -> float:
    """Implied probability (%) -> decimal odds."""
    return round(100 / p, 4)


def share_to_decimal(cents: float) -> float:
    """Prediction-market share price (0-1) -> decimal odds."""
    if not 0 < cents < 1:
        raise ValueError("share price must be between 0 and 1")
    return round(1 / cents, 3)


def decimal_to_share(d: float) -> float:
    """Decimal odds -> share price (0-1)."""
    return round(1 / d, 4)


if __name__ == "__main__":
    for d in (1.493, 2.857, 14.286):
        print(d, decimal_to_american(d), decimal_to_fractional(d),
              f"{decimal_to_probability(d)}%")
    print("share 0.67 ->", share_to_decimal(0.67), "decimal")

Running it prints the exact values OddsPapi returns for a live fixture (verified below):

1.493 -203 35/71 66.98%
2.857 +186 13/7 35.0%
14.286 +1329 93/7 7.0%
share 0.67 -> 1.493 decimal

Verifying Against Live Data

Here is the part most “odds converter” tutorials skip: testing the math against a real bookmaker feed. We pulled a live Brazilian Serie B fixture (Avaí vs Cuiabá) from the OddsPapi /v4/odds endpoint, with Polymarket priced on the Full Time Result (1X2) market. Auth is a query parameter, never a header.

import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"

params = {
    "apiKey": API_KEY,
    "fixtureId": "id1000039068822998",
    "bookmakers": "polymarket,pinnacle,draftkings",
}
data = requests.get(f"{BASE}/odds", params=params).json()

# Full Time Result is market 101; outcomes 101=Home, 102=Draw, 103=Away
pm = data["bookmakerOdds"]["polymarket"]["markets"]["101"]["outcomes"]
for outcome_id, outcome in pm.items():
    px = outcome["players"]["0"]
    print(outcome_id, px["price"], px["priceAmerican"], px["priceFractional"])

Output, captured live on Jun 21, 2026:

101 1.493 -203 35/71      # Avai (home)
102 2.857 +186  13/7      # Draw
103 14.286 +1329 93/7     # Cuiaba (away)

The feed hands you price (decimal), priceAmerican, and priceFractional in one shot. Note the American and fractional fields are strings, so cast before you do math on them. Your converter and the feed agree to the cent, which is exactly what you want to confirm before trusting either.

The Conversion Everyone Gets Wrong: Polymarket Shares

Prediction markets are the reason this post leads with Polymarket. A market like Polymarket or Kalshi does not quote odds at all. It quotes a share price between 0 and 1 that reads directly as an implied probability. A YES share trading at 0.67 means the market prices that outcome at 67%, which is decimal odds of 1 / 0.67 = 1.493.

OddsPapi exposes the raw share price inside exchangeMeta as the cents field, alongside the depth-of-book ladder. Here is the back side of that same Polymarket outcome, straight from the feed:

back = data["bookmakerOdds"]["polymarket"]["markets"]["101"] \
           ["outcomes"]["101"]["players"]["0"]["exchangeMeta"]["back"]

for level in back:
    print(f"share {level['cents']}  ->  decimal {level['price']}  "
          f"(size {level['size']})")
share 0.67  ->  decimal 1.493  (size 60.6)
share 0.72  ->  decimal 1.389  (size 314.0)
share 0.78  ->  decimal 1.282  (size 55.0)

Every level confirms the rule: decimal = 1 / share. The feed has already done the inversion in the price field, so unlike scraping the Polymarket CLOB directly, you never have to convert share prices yourself. If you do pull raw shares from elsewhere, share_to_decimal() above is all you need. For the full Gamma vs CLOB breakdown, see our Polymarket API deep dive and the Kalshi vs Polymarket comparison.

Stripping the Vig: From Implied Probability to True Odds

One trap worth calling out: decimal_to_probability() gives you the implied probability, which still contains the bookmaker margin. Add the three 1X2 probabilities from any single book and they sum to more than 100% — that overround is the vig. To get a fair probability you must de-vig. The simplest two-outcome version normalises the implied probabilities so they sum to 1:

def devig_proportional(decimals: list[float]) -> list[float]:
    """Normalise implied probabilities to remove the overround."""
    implied = [1 / d for d in decimals]
    total = sum(implied)
    return [round(p / total, 4) for p in implied]  # fair probabilities


# Three-way 1X2 example
fair = devig_proportional([1.493, 2.857, 14.286])
print(fair)  # -> [0.6539, 0.3417, 0.0683]  (sums to 1.0)

Now those probabilities are usable for expected-value math. For a sharper benchmark across hundreds of books, see our consensus odds calculator and the vig calculator, which measures margin across 350+ books instead of trusting one.

Why Let the Feed Do It

A converter module is twenty lines and worth keeping for offline data. But for live work, the feed that already ships every format is the bigger win:

  • 350+ bookmakers — sharps (Pinnacle, SBOBet), US books (DraftKings, FanDuel), and exchanges (Betfair, Polymarket, Kalshi) all normalised to the same shape.
  • Free historical odds — pull the full price history of any fixture on the free tier, already converted, for backtesting.
  • Prediction markets in decimal — Polymarket and Kalshi share prices inverted to odds for you, so cross-book comparison just works.

You stop maintaining converters and start shipping the actual analysis. Pair this with line shopping across 350+ books to see the formats in action.

Frequently Asked Questions

How do I convert American odds to decimal in Python?

For positive American odds, decimal = 1 + (american / 100). For negative, decimal = 1 + (100 / abs(american)). So +186 becomes 2.86 and -203 becomes 1.493. The american_to_decimal() function above handles both branches.

How do I convert a Polymarket share price to odds?

A Polymarket share price is a probability between 0 and 1. Decimal odds = 1 / share price, so a 0.67 share is 1.493 in decimal. OddsPapi already returns Polymarket prices as decimal odds in the price field, so no conversion is needed when you use the API.

What is the difference between implied probability and true probability?

Implied probability (1 / decimal odds) includes the bookmaker’s margin, so a book’s outcomes sum to more than 100%. True (fair) probability removes that overround by normalising the implied probabilities to sum to 1, as in the devig_proportional() example.

Does OddsPapi convert odds formats for me?

Yes. Every price in the OddsPapi feed ships as decimal (price), American (priceAmerican), and fractional (priceFractional) simultaneously, across 350+ bookmakers. The American and fractional fields are strings, so cast them before doing math.

Is fractional odds conversion exact?

No, fractional odds are an approximation of the decimal price using the nearest small-denominator fraction (1.493 becomes 35/71, not 493/1000). Use Fraction(d - 1).limit_denominator(100), or just take the canonical priceFractional string the feed provides.

Start Converting Less, Shipping More

The converter module above is yours to keep. But the real time-saver is a feed that hands you every format, including prediction-market shares as decimal odds, across 350+ books. Get your free OddsPapi API key and stop writing converters.