FPL Fixture Difficulty (FDR) from the API in Python
Read FPL fixture difficulty from the free API: team_h_difficulty, team_a_difficulty, how they map to team strength, and a Python table of the next 5 gameweeks.
Published · Updated · By Omar Eldeeb
FPL fixture difficulty ratings are in https://fantasy.premierleague.com/api/fixtures/: every fixture carries team_h_difficulty and team_a_difficulty on a 1–5 scale, where higher is harder. Use fixtures/?future=1 for upcoming matches only, and map team IDs to names with bootstrap-static/ → teams. No key is needed, but there's no CORS header, so call it from a script.
Below: the fields, what the rating appears to be based on, and a Python script that prints every team's next five fixtures with difficulty, run on 2026-10-14 (2026/27 season, before gameweek 7).
Where the ratings are
| Endpoint | Field | Point of view |
|---|---|---|
fixtures/ (add ?future=1 or ?event={gw}) |
team_h_difficulty, team_a_difficulty |
Home side and away side |
element-summary/{player_id}/ → fixtures |
difficulty, is_home |
The player's team |
bootstrap-static/ → teams |
strength_overall_home, strength_overall_away |
The ratings the difficulties match (see below) |
A fixture row looks like this (gameweek 7, from the live API):
{"id": 64, "event": 7, "kickoff_time": "2026-10-17T11:30:00Z", "team_h": 9, "team_a": 6,
"team_h_difficulty": 4, "team_a_difficulty": 3, "finished": false, "started": false}Next five gameweeks, easiest first
import requests
API = "https://fantasy.premierleague.com/api/"
s = requests.Session()
s.headers["User-Agent"] = "fpl-fdr/1.0 (personal script)"
boot = s.get(API + "bootstrap-static/", timeout=30).json()
teams = {t["id"]: t["short_name"] for t in boot["teams"]}
next_gw = next(e["id"] for e in boot["events"] if e["is_next"])
horizon = range(next_gw, next_gw + 5) # the next five gameweeks
fixtures = s.get(API + "fixtures/?future=1", timeout=30).json()
# One row per team: list of (gw, opponent, H/A, difficulty), from that team's point of view.
runs = {tid: [] for tid in teams}
for f in fixtures:
if f["event"] not in horizon: # event is None for unscheduled games
continue
runs[f["team_h"]].append((f["event"], teams[f["team_a"]], "H", f["team_h_difficulty"]))
runs[f["team_a"]].append((f["event"], teams[f["team_h"]], "A", f["team_a_difficulty"]))
table = []
for tid, games in runs.items():
games.sort()
total = sum(g[3] for g in games)
# Blank gameweeks lower the sum, so rank on the average and show the game count.
table.append((total / len(games) if games else 99, len(games), teams[tid],
" ".join(f"{opp}({ha}){d}" for _, opp, ha, d in games)))
print(f"Fixture difficulty, GW{horizon.start}-GW{horizon.stop - 1} (easiest first)")
for avg, n, team, run in sorted(table):
print(f"{team:<4} avg {avg:.1f} over {n}: {run}")
unscheduled = sum(1 for f in fixtures if f["event"] is None)
print(f"\nunscheduled future fixtures (event is null): {unscheduled}")Output on 2026-10-14:
Fixture difficulty, GW7-GW11 (easiest first)
COV avg 2.6 over 5: TOT(A)3 FUL(H)2 SUN(H)3 EVE(A)3 CRY(H)2
MCI avg 2.6 over 5: IPS(H)2 AVL(A)3 BHA(H)3 NFO(A)3 FUL(H)2
TOT avg 2.6 over 5: COV(H)2 CHE(A)4 CRY(H)2 LEE(A)3 IPS(H)2
ARS avg 3.0 over 5: NFO(A)3 EVE(H)3 LIV(A)4 HUL(H)2 NEW(A)3
BOU avg 3.0 over 5: SUN(H)3 MUN(A)4 LEE(H)3 IPS(A)2 NFO(H)3
CHE avg 3.0 over 5: EVE(A)3 TOT(H)2 MUN(H)4 SUN(A)3 LEE(H)3
FUL avg 3.0 over 5: HUL(H)2 COV(A)2 AVL(A)3 NEW(H)3 MCI(A)5
SUN avg 3.0 over 5: BOU(A)3 LEE(H)3 COV(A)2 CHE(H)4 AVL(A)3
AVL avg 3.2 over 5: NEW(A)3 MCI(H)4 FUL(H)2 MUN(A)4 SUN(H)3
BHA avg 3.2 over 5: CRY(H)2 LIV(A)4 MCI(A)5 BRE(H)3 HUL(A)2
BRE avg 3.2 over 5: LIV(H)4 HUL(A)2 NFO(H)3 BHA(A)4 EVE(H)3
CRY avg 3.2 over 5: BHA(A)4 NEW(H)3 TOT(A)3 LIV(H)4 COV(A)2
HUL avg 3.2 over 5: FUL(A)3 BRE(H)3 IPS(H)2 ARS(A)5 BHA(H)3
IPS avg 3.2 over 5: MCI(A)5 NFO(H)3 HUL(A)2 BOU(H)3 TOT(A)3
LEE avg 3.2 over 5: MUN(H)4 SUN(A)3 BOU(A)3 TOT(H)2 CHE(A)4
NEW avg 3.2 over 5: AVL(H)3 CRY(A)3 EVE(H)3 FUL(A)3 ARS(H)4
NFO avg 3.2 over 5: ARS(H)4 IPS(A)2 BRE(A)3 MCI(H)4 BOU(A)3
EVE avg 3.4 over 5: CHE(H)4 ARS(A)5 NEW(A)3 COV(H)2 BRE(A)3
LIV avg 3.4 over 5: BRE(A)3 BHA(H)3 ARS(H)4 CRY(A)3 MUN(H)4
MUN avg 3.4 over 5: LEE(A)3 BOU(H)3 CHE(A)4 AVL(H)3 LIV(A)4
unscheduled future fixtures (event is null): 0As a cross-check, element-summary/101/ (a Chelsea player) listed EVE (A) 3, TOT (H) 2, MUN (H) 4, SUN (A) 3 and LEE (H) 3 for gameweeks 7 to 11, the same as the CHE row.
What the rating is based on
FPL doesn't document the formula. The data shows a simple relationship, at least in 2026/27:
from collections import Counter
import requests
API = "https://fantasy.premierleague.com/api/"
h = {"User-Agent": "fpl-fdr/1.0 (personal script)"}
teams = {t["id"]: t for t in requests.get(API + "bootstrap-static/", headers=h, timeout=30).json()["teams"]}
fixtures = requests.get(API + "fixtures/", headers=h, timeout=30).json()
print("difficulty values:", sorted(Counter(
d for f in fixtures for d in (f["team_h_difficulty"], f["team_a_difficulty"])).items()))
print("home side's difficulty == away team's strength_overall_home:",
sum(f["team_h_difficulty"] == teams[f["team_a"]]["strength_overall_home"] for f in fixtures), "of", len(fixtures))
print("away side's difficulty == home team's strength_overall_away:",
sum(f["team_a_difficulty"] == teams[f["team_h"]]["strength_overall_away"] for f in fixtures), "of", len(fixtures))
ars = next(t for t in teams.values() if t["short_name"] == "ARS")
print("ARS:", {k: ars[k] for k in ("strength", "strength_overall_home", "strength_overall_away",
"strength_attack_home", "strength_defence_away")})difficulty values: [(2, 171), (3, 380), (4, 171), (5, 38)]
home side's difficulty == away team's strength_overall_home: 380 of 380
away side's difficulty == home team's strength_overall_away: 380 of 380
ARS: {'strength': None, 'strength_overall_home': 4, 'strength_overall_away': 5, 'strength_attack_home': 0, 'strength_defence_away': 0}What that means:
- Each team has two ratings, and they describe the team as an opponent. Arsenal's
strength_overall_homeis 4 andstrength_overall_awayis 5. Hosting Arsenal is rated 4; visiting Arsenal is rated 5. The field names read as if they described Arsenal's own home and away strength, but the fixtures say otherwise. - The rating doesn't change with form within a fixture list. It's a lookup per opponent and venue, so two teams playing the same opponent at the same venue get the same number. FPL may update the ratings during a season, so re-read
fixtures/rather than caching it for months. - This season uses 2 to 5. No fixture was rated 1 on 2026-10-14.
- Some team fields are empty.
strengthwasnulland thestrength_attack_*/strength_defence_*fields were 0 for every team, so build nothing on them this season.
Pitfalls
- Blank and double gameweeks. A team with no match in a gameweek contributes nothing to the sum, which makes its run look easier. The script averages over games played and shows the count. A double gameweek adds a sixth fixture to the five-week window; that's two chances to score, not a harder run.
event: null. Postponed or unscheduled fixtures have no gameweek. The script skips them and counts them at the end.- Difficulty isn't attack and defence. A defender cares about the opponent's attack, a forward about its defence. FPL's single number blends both. For something sharper, combine fixtures with expected goals from
element-summaryhistory or another source. - No CORS. Server-side only.
Related guides for this API
- FPL API endpoints: complete reference.
- FPL live gameweek points from the API.
- Find your FPL manager ID and pull your team history.
- FPL API price change data in Python.
Do it without code
The free FPL query builder builds a ready-to-run input for the gameweek-picks mode, which weighs upcoming fixtures, and shows a fixed example of the output shape. It doesn't fetch live FPL data in your browser.
At scale
The FPL Intelligence actor on Apify uses these difficulty ratings in its gameweek_picks mode, with a configurable fixture look-ahead (picksFixtureLookahead), to rank captain, transfer-in, differential and sell candidates. It also runs a wildcard optimizer and mini-league and live-gameweek modes, and exports JSON, CSV or Excel. It's free to start, then pay-as-you-go.
Disclosure: I build the datatooly query builder and the Apify actor. The scripts above need neither.
Sources: live responses from fantasy.premierleague.com/api/ on 2026-10-14. FPL publishes no API documentation or difficulty formula.
Quick answers
Where is fixture difficulty in the FPL API?
In https://fantasy.premierleague.com/api/fixtures/. Each fixture has team_h_difficulty (how hard the match is for the home team) and team_a_difficulty (for the away team), on FPL's 1 to 5 scale. element-summary/{player_id}/ repeats it from one player's side as difficulty, alongside is_home.
How is FPL fixture difficulty calculated?
FPL doesn't publish a formula. In the 2026/27 data, every fixture's difficulty equalled the opponent's strength_overall_home (when you play at home) or strength_overall_away (when you play away) from bootstrap-static/ → teams: 380 of 380 fixtures for both sides on 2026-10-14. So it's a fixed rating per opponent and venue, not a model of form.
How do I get only upcoming FPL fixtures?
Call fixtures/?future=1; on 2026-10-14 it returned the 320 fixtures not yet played. Use fixtures/?event=7 for one gameweek. A fixture with event: null hasn't been given a gameweek yet.