The Problem
Soccer analytics work needs data from many sources—FBref for stats, Club Elo for ratings, Understat for expected goals—but each site has different HTML, column names, and identifiers. Teams end up writing and maintaining one scraper per site, and every site redesign breaks them. soccerdata solves that by wrapping nine scrapers behind one consistent Python API that returns Pandas DataFrames with matching schemas.
What This Does
soccerdata is a Python package with one reader class per data source: FBref, ESPN, ClubElo, FotMob, Sofascore, SoFIFA, Understat, WhoScored, and MatchHistory, each in its own module under soccerdata/. You instantiate a scraper with a league and season, then call methods like read_schedule() or read_team_season_stats() to get DataFrames. Downloads are cached locally, so repeated reads don't re-hit the network.
This is a fork of probberechts/soccerdata (2,040 stars upstream) with 0 stars of its own. The core value is unchanged: one import, one API, nine sources.
How It Is Wired
Execution starts at the public class constructors—sd.FBref('ENG-Premier League', '2021')—which live in the per-source modules. Every reader inherits from BaseRequestsReader in soccerdata/_common.py, which owns the HTTP fetching, caching, and file I/O. That module is the hub: 10 of the 27 internal modules import it, and it contains 36 functions and 4 classes.
The highest-blast-radius function is parse, called from 27 places across the codebase—every source module routes its HTML/JSON through it. read_seasons (10 callers), _is_complete (9), and read_schedule (9) follow. _common.py is also where the file effects live: it reads and writes cached downloads, as do understat.py and clubelo.py. The module graph shows no circular dependencies, but _common.py sits at instability 0.6—it's both heavily imported and imports widely, so changing it touches most of the package.
How To Use It
pip install soccerdata
# or, since uv.lock is present: uv sync
import soccerdata as sd
fbref = sd.FBref('ENG-Premier League', '2021')
games = fbref.read_schedule()
team_stats = fbref.read_team_season_stats(stat_type="passing")
No environment variables or API keys are required—the scrapers hit public pages. The README documents these commands verbatim. Note the usage notice: scraping must comply with each site's terms of service.
Real-World Use
A match-prediction pipeline: pull fixtures from FBref.read_schedule(), merge in ClubElo.read_team_history() for ratings, add Understat.read_shot_events() for expected goals, then feed the joined DataFrame to a model. The consistent column naming across sources is what makes the merge practical rather than a data-cleaning project.
Code Health & Issues
Static analysis found 16 issues (6 high, 10 medium) across 5 kinds:
- High - Duplicated code blocks - 74 repeated 6-line blocks across 11 files, including
_common.py,clubelo.py,espn.py,fbref.py. Fix: extract shared helpers. - High - Deep nesting - max indentation depth 7 in
_common.py,espn.py,fbref.py. Fix: guard clauses and early returns. - Medium - Oversized files -
_common.pyat 682 lines, plusfbref.pyandunderstat.py. Fix: split by responsibility. - Medium - Broad exception handling in
_common.pyswallows errors indiscriminately. - Medium - Files opened without context managers in
_common.pyandunderstat.py.
SDLC observations from the workflow files: GitHub Actions are pinned to version tags, not commit SHAs (a supply-chain risk); CI pushes directly to the default branch; no permissions block declared on workflows; no dependency vulnerability scan in CI. Tests exist (17 files) and CI runs on GitHub Actions, but the CI itself needs hardening.
The Bottom Line
A genuinely useful library—the unified API across nine soccer data sources is the kind of thing that saves teams weeks of scraper maintenance. The fork adds nothing over upstream so far, and the code quality issues concentrate in _common.py, which is exactly the file you'd need to touch for most changes. Worth using as-is; worth fixing the CI before trusting it with secrets.