- About
- Installation & Quick Start
- Configuration
- API Modules
- Advanced Usage
- Examples & Wiki
- Development
- Contact
NHL-api-py is a Python package that provides a simple wrapper around the NHL API, allowing you to easily access and retrieve NHL data in your Python applications.
Note: I created this to help me with some machine learning projects around the NHL and the NHL data sets. Special thanks to https://github.com/erunion/sport-api-specifications/tree/master/nhl, https://gitlab.com/dword4/nhlapi/-/blob/master/stats-api.md and https://github.com/Zmalski/NHL-API-Reference.
This started as a project around the old NHL API, that got deprecated in 2023(?). This project has evolved to adopt the new NHL API(s) which are undocumented but discoverable via the NHL platforms. Some endpoints are still being added, and some are still being discovered. The goal is to have a complete wrapper around the NHL API.
If you find any, open a ticket or post in the discussions tab. I would love to hear more.
pip install nhl-api-py
# - OR -
uv add nhl-api-pyfromnhlpyimportNHLClient# Basic usageclient=NHLClient()
# Get all teamsteams=client.teams.teams()
# Get current standingsstandings=client.standings.league_standings()
# Get today's gamesgames=client.schedule.daily_schedule()fromnhlpyimportNHLClient# Default configurationclient=NHLClient()
# With debug loggingclient=NHLClient(debug=True)
# All available configurationsclient=NHLClient(
debug=True, # Enable debug loggingtimeout=30, # Request timeout in secondsssl_verify=True, # SSL certificate verificationfollow_redirects=True# Follow HTTP redirects
)These need to updated with v3 updates
More in depth examples can be found in the wiki, feel free to add more: Examples
Im available on Bluesky or the twit for any questions or just general chats about enhancements.
This project is organized into several sub modules, each representing a different endpoint of the NHL API.
They are grouped by function to make the library easier to navigate and use. The main modules are:
teams: Contains endpoints related to NHL teams, including team information and rosters. You can find team_id(s) here along with franchise_id(s) needed for some of the stats queries.schedule: Contains endpoints related to the NHL schedule, including game dates, weekly schedules, and team schedules.stats: Contains endpoints related to player and team statistics. This will have your basic stats, summary stats and more advanced stats using the new query builder, which allows for more complex queries to be built up programmatically.edge: Contains endpoints to access EDGE related player and team data (shot speed, skate speed, etc). These were well hidden endpoints from the NHL and behave a bit differently with their response payloads.standings: Contains endpoints related to NHL standings, including current standings and historical standings.game_center: Contains endpoints related to game center data, including box scores, play-by-play data, and game summaries.misc: Contains miscellaneous endpoints that don't fit into the other categories, such as glossary terms, configuration data, and country information.players: Get Players by team and prospects.
helpers: Contains helper functions and utilities for working with the NHL API, such as getting game IDs by season or calculating player statistics. These are experimental and often times make many requests, can return DataFrames or do calculations. Stuff I find myself doing over and over I tend to move into helpers for convenience. They are often cross domain, involve many sub requests, may integrate more machine learning techniques, or just make it easier to get the data you want. These will have built in sleeping to avoid hitting the API too hard, but you can override this by setting thesleepparameter toFalsein the function call.
Do you have a specific use case or cool code snippet you use over and over? If its helpful to others please open a PR and add a helper.
Get information about NHL teams, rosters, and franchises.
# Get current teamsteams=client.teams.teams()
# Get teams from a specific dateteams=client.teams.teams(data="2024-10-04")# Get current season rosterroster=client.teams.team_roster(team_abbr="BUF", season="20242025")# Get all franchises (current and historical)franchises=client.teams.franchises()fromnhlpyimportNHLClientclient=NHLClient()
# Get all current teamsteams=client.teams.teams()
# Find a specific teamforteaminteams:
ifteam['abbr'] =='TOR':
print(f"Team: {team['name']}")
print(f"Division: {team['division']['name']}")
print(f"Franchise ID: {team['franchise_id']}")
break# Get that team's rosterroster=client.teams.team_roster(team_abbr="TOR", season="20242025")
print(f"Forwards: {len(roster['forwards'])}")
print(f"Defensemen: {len(roster['defensemen'])}")
print(f"Goalies: {len(roster['goalies'])}")Get NHL game schedules - daily, weekly, or team-specific schedules.
# Get today's gamesgames=client.schedule.daily_schedule()
# Get games for a specific dategames=client.schedule.daily_schedule(date="2024-01-01")# Get this week's gamesweek_games=client.schedule.weekly_schedule()
# Get games for a specific weekweek_games=client.schedule.weekly_schedule(date="2024-01-01")# Get team's games for a specific monthteam_schedule=client.schedule.team_monthly_schedule(team_abbr="BUF", month="2024-10")# Defaults to 'now'weekly_schedule=client.schedule.team_weekly_schedule(team_abbr="BUF")
weekly_schedule=client.schedule.team_weekly_schedule(team_abbr="BUF", date="2024-01-01")full_schedule=client.schedule.team_season_schedule(team_abbr="BUF", season="20242025")Note: This is an endpoint available from the NHL but why it exists is a different story.
I seems to return the same data as the above endpoints
cal=client.schedule.calendar_schedule(date="2024-01-01")games=client.schedule.playoff_carousel(season="20242025")series=client.schedule.playoff_series_schedule(season="20242025", series="a")bracket=client.schedule.playoff_bracket(year="2024")fromnhlpyimportNHLClientclient=NHLClient()
# Get games by date. Leave date blank to get today's gamesgames=client.schedule.daily_schedule(date="2024-10-08")
# Show what's on tonightforgameingames.get('games', []):
away_team=game['awayTeam']['abbrev']
home_team=game['homeTeam']['abbrev']
game_time=game['startTimeUTC']
print(f"{away_team} @ {home_team} - {game_time}")Response Examples
daily_schedule() response:
'date': '2024-10-08','games': [
{
'awayTeam': {'abbrev': 'STL','awaySplitSquad': False,'commonName': {'default': 'Blues'
},
'darkLogo': 'https://assets.nhle.com/logos/nhl/svg/STL_20082009-20242025_dark.svg','id': 19,'logo': 'https://assets.nhle.com/logos/nhl/svg/STL_20082009-20242025_light.svg','placeName': {'default': 'St. Louis'},'placeNameWithPreposition': {'default': 'St. Louis','fr': 'de St. Louis'},'score': 3},'easternUTCOffset': '-04:00','gameCenterLink': '/gamecenter/stl-vs-sea/2024/10/08/2024020003','gameOutcome': {'lastPeriodType': 'REG'},'gameScheduleState': 'OK','gameState': 'OFF','gameType': 2,'homeTeam': {'abbrev': 'SEA','commonName': {'default': 'Kraken'},'darkLogo': 'https://assets.nhle.com/logos/nhl/svg/SEA_dark.svg','homeSplitSquad': False,'id': 55,'logo': 'https://assets.nhle.com/logos/nhl/svg/SEA_light.svg','placeName': {'default': 'Seattle'},'placeNameWithPreposition': {'default': 'Seattle','fr': 'de Seattle'},'score': 2},.....}]STL @ SEA - 2024-10-08T20:30:00ZBOS @ FLA - 2024-10-08T23:00:00ZCHI @ UTA - 2024-10-09T02:00:00ZGet player and team statistics - from basic season stats to advanced analytics.
# Get a player's full career statisticscareer_stats=client.stats.player_career_stats(player_id="8478402") # Connor McDavid# Get game-by-game stats for a playergame_log=client.stats.player_game_log(
player_id="8478402", season_id="20232024", game_type=2# Regular season
)# Get team stats for a seasonteam_stats=client.stats.team_summary(
start_season="20232024", end_season="20232024"
)# Get basic skater statsskater_stats=client.stats.skater_stats_summary(
start_season="20232024", end_season="20232024"
)
# Filter by franchise (team)skater_stats=client.stats.skater_stats_summary(
start_season="20232024", end_season="20232024",
franchise_id="10"# Toronto Maple Leafs
)See Query Builder for more advanced queries.
# Get basic goalie statsgoalie_stats=client.stats.goalie_stats_summary(
start_season="20232024", end_season="20232024"
)
# Get advanced goalie statsgoalie_stats=client.stats.goalie_stats_summary(
start_season="20232024",
end_season="20232024",
stats_type="advanced"
)fromnhlpyimportNHLClientclient=NHLClient()
# Get current season skater statsstats=client.stats.skater_stats_summary(
start_season="20242025", end_season="20242025"
)
# Show top 5 scorersfori, playerinenumerate(stats[:5]):
print(f"{i+1}. {player['skaterFullName']}: {player['points']} points")
1.NikitaKucherov: 121points2.NathanMacKinnon: 116points3.LeonDraisaitl: 106points4.DavidPastrnak: 106points5.MitchMarner: 102pointsAccess NHL EDGE statistics - advanced player and puck tracking data including shot speed, skating speed, distance traveled, and zone time metrics. This data provides insights beyond traditional statistics.
EDGE stats capture real-time tracking data from NHL games:
- Skater Data: Shot velocity, skating speed, burst counts, distance covered, zone time
- Goalie Data: Save locations, shot tracking, 5v5 performance breakdowns
- Team Data: Aggregate team skating metrics, shot patterns, and zone possession
Use EDGE endpoints when you need physical performance metrics. Use the Stats endpoints for traditional statistics like goals, assists, and points.
All EDGE methods support two patterns:
- Current season: Omit the
seasonparameter or passNoneto get current data - Specific season: Pass season as 8-digit string (e.g.,
"20232024")
The game_type parameter defaults to 2 (regular season) but can be set to 3 for playoffs.
These endpoints may be unstable and change frequently. They appear to be designed for internal NHL use for broadcast graphics. Response structures can vary and data availability may be inconsistent.
# Get comprehensive EDGE statistics for a skaterclient.edge.skater_detail(player_id='8478402', season='20252026')
# Get shot speed analytics (max speed, average speed)client.edge.skater_shot_speed_detail(player_id='8478402', season='20252026')
# Get skating speed metrics (burst counts, max speed)client.edge.skater_skating_speed_detail(player_id='8478402', season='20252026')
# Get shot location patterns and heat mapsclient.edge.skater_shot_location_detail(player_id='8478402', season='20252026')
# Get distance traveled per game and per shiftclient.edge.skater_skating_distance_detail(player_id='8478402', season='20252026')
# Get comparison data vs league averagesclient.edge.skater_comparison(player_id='8478402', season='20252026')
# Get time spent in each zone (offensive/defensive/neutral)client.edge.skater_zone_time(player_id='8478402', season='20252026')
# Get league-wide skater EDGE statistics overviewclient.edge.skater_landing(season='20252026')
# Get CAT (Catch All Tracking) EDGE statisticsclient.edge.cat_skater_detail(player_id='8478402', season='20252026')# Get comprehensive EDGE statistics for a goalieclient.edge.goalie_detail(player_id='8476945', season='20252026')
# Get shot location data and save percentages by zoneclient.edge.goalie_shot_location_detail(player_id='8476945', season='20252026')
# Get 5-on-5 performance statisticsclient.edge.goalie_5v5_detail(player_id='8476945', season='20252026')
# Get comparison data vs league averagesclient.edge.goalie_comparison(player_id='8476945', season='20252026')
# Get detailed save percentage breakdowns by situationclient.edge.goalie_save_percentage_detail(player_id='8476945', season='20252026')
# Get league-wide goalie EDGE statistics overviewclient.edge.goalie_landing(season='20252026')
# Get CAT (Catch All Tracking) EDGE statisticsclient.edge.cat_goalie_detail(player_id='8476945', season='20252026')# Get comprehensive EDGE statistics for a teamclient.edge.team_detail(team_id='19', season='20252026')
# Get team skating distance statisticsclient.edge.team_skating_distance_detail(team_id='19', season='20252026')
# Get team zone time breakdownsclient.edge.team_zone_time_details(team_id='19', season='20252026')
# Get team shot location patternsclient.edge.team_shot_location_detail(team_id='19', season='20252026')
# Get team shot speed analyticsclient.edge.team_shot_speed_detail(team_id='19', season='20252026')
# Get team skating speed metricsclient.edge.team_skating_speed_detail(team_id='19', season='20252026')
# Get league-wide team EDGE statistics overviewclient.edge.team_landing(season='20252026')Get current league standings or standings from any point in NHL history.
# Get current league standingsstandings=client.standings.league_standings()# Get standings from a specific datestandings=client.standings.league_standings(date="2024-01-01")
# Get final standings for a completed seasonstandings=client.standings.league_standings(season="20232024")# Get metadata about seasons (dates, rules, etc.)season_info=client.standings.season_standing_manifest()fromnhlpyimportNHLClientclient=NHLClient()
# Get current standingsstandings=client.standings.league_standings()
# Find division leadersdivisions= {}
forteaminstandings['standings']:
division=team['divisionName']
ifdivisionnotindivisions:
divisions[division] =teamelifteam['points'] >divisions[division]['points']:
divisions[division] =team# Display division leadersfordivision, teamindivisions.items():
print(f"{division}: {team['teamName']['default']} ({team['points']} pts)")Get detailed game data - boxscores, play-by-play, and live game information.
# Get complete boxscore for a gameboxscore=client.game_center.boxscore(game_id="2023020280")# Get detailed play-by-play dataplay_by_play=client.game_center.play_by_play(game_id="2023020280")# Get game matchup info and key statsgame_info=client.game_center.match_up(game_id="2023020280")# Get today's scoresscores=client.game_center.daily_scores()
# Get scores for a specific datescores=client.game_center.daily_scores(date="2024-01-01")# Get shift chart datashifts=client.game_center.shift_chart_data(game_id="2023020280")
# Get additional game statsstats=client.game_center.season_series_matchup(game_id="2023020280")
# Get game story/recapstory=client.game_center.game_story(game_id="2023020280")fromnhlpyimportNHLClientclient=NHLClient()
# Get a game's complete datagame_id="2023020280"boxscore=client.game_center.boxscore(game_id)
play_by_play=client.game_center.play_by_play(game_id)
# Analyze the gameaway_team=boxscore['awayTeam']['abbrev']
home_team=boxscore['homeTeam']['abbrev']
away_score=boxscore['awayTeam']['score']
home_score=boxscore['homeTeam']['score']
print(f"Final: {away_team}{away_score} - {home_team}{home_score}")
# Count shots by periodshots_by_period= {}
forplayinplay_by_play.get('plays', []):
ifplay['typeDescKey'] =='shot-on-goal':
period=play['periodDescriptor'].get('number', 'Unknown')
ifperiodnotinshots_by_period:
shots_by_period[period] =0shots_by_period[period] +=1forperiod, shotsinshots_by_period.items():
print(f"Period {period}: {shots} shots")Utility endpoints for NHL reference data and configuration information.
# Get definitions of NHL terms and statisticsglossary=client.misc.glossary()# Get available filter options and API configurationconfig=client.misc.config()# Get list of countries in NHL datacountries=client.misc.countries()# Get season-specific rules and informationseason_info=client.misc.season_specific_rules_and_info()# Get draft years and round informationdraft_info=client.misc.draft_year_and_rounds()fromnhlpyimportNHLClientclient=NHLClient()
# Get NHL glossaryglossary=client.misc.glossary()
# Find specific stats definitionsstat_terms= ["BENCH", "BKS", "A", "ENA", "EV GA"]
forterminstat_terms:
forentryinglossary:
ifentry['abbreviation'].upper() ==term:
print(f"{term}: {entry['definition']}")
continueThe skater stats endpoint can be accessed using the new query builder. It should make creating and understanding the queries a bit easier. Filters are being added as I go, and will match up to what the NHL API will allow.
The idea is to easily, and programatically, build up more complex queries using the query filters. A quick example below:
filters= [
GameTypeQuery(game_type="2"),
DraftQuery(year="2020", draft_round="2"),
SeasonQuery(season_start="20202021", season_end="20232024"),
PositionQuery(position=PositionTypes.ALL_FORWARDS)
]The sorting is a list of dictionaries similar to below. You can supply your own, otherwise it will
default to the default sort properties that the stat dashboard uses. All sorting defaults are found
in the nhl-api-py/nhlpy/api/query/sorting/sorting_options.py file.
Default Sorting
skater_summary_default_sorting= [
{"property": "points", "direction": "DESC"},
{"property": "gamesPlayed", "direction": "ASC"},
{"property": "playerId", "direction": "ASC"},
]The following report types are available. These are used to build the request url. So /summary, /bios, etc.
summary
bios
faceoffpercentages
faceoffwins
goalsForAgainst
realtime
penalties
penaltykill
penaltyShots
powerplay
puckPossessions
summaryshooting
percentages
scoringRates
scoringpergame
shootout
shottype
timeonicefromnhlpy.api.query.filters.franchiseimportFranchiseQueryfromnhlpy.api.query.filters.shoot_catchimportShootCatchesQueryfromnhlpy.api.query.filters.draftimportDraftQueryfromnhlpy.api.query.filters.seasonimportSeasonQueryfromnhlpy.api.query.filters.game_typeimportGameTypeQueryfromnhlpy.api.query.filters.positionimportPositionQuery, PositionTypesfromnhlpy.api.query.filters.statusimportStatusQueryfromnhlpy.api.query.filters.opponentimportOpponentQueryfromnhlpy.api.query.filters.home_roadimportHomeRoadQueryfromnhlpy.api.query.filters.experienceimportExperienceQueryfromnhlpy.api.query.filters.decisionimportDecisionQueryfilters= [
GameTypeQuery(game_type="2"),
DraftQuery(year="2020", draft_round="2"),
SeasonQuery(season_start="20202021", season_end="20232024"),
PositionQuery(position=PositionTypes.ALL_FORWARDS),
ShootCatchesQuery(shoot_catch="L"),
HomeRoadQuery(home_road="H"),
FranchiseQuery(franchise_id="1"),
StatusQuery(is_active=True),#for active players OR for HOF players StatusQuery(is_hall_of_fame=True),OpponentQuery(opponent_franchise_id="2"),
ExperienceQuery(is_rookie=True), # for rookies || ExperienceQuery(is_rookie=False) #for veteranDecisionQuery(decision="W") # OR DecisionQuery(decision="L") OR DecisionQuery(decision="O")
]Show me all players, during the regular season (game_type=2), that were drafted in 2020 (DraftQuery)
for the 2020-2021 season through 2023-2024 season (SeasonQuery) that play forward: LW, C, RW (PositionQuery). Use
summary statistics and aggregate (aggregate=True) all the years together.
fromnhlpy.api.query.builderimportQueryBuilder, QueryContextfromnhlpy.nhl_clientimportNHLClientfromnhlpy.api.query.filters.draftimportDraftQueryfromnhlpy.api.query.filters.seasonimportSeasonQueryfromnhlpy.api.query.filters.game_typeimportGameTypeQueryfromnhlpy.api.query.filters.positionimportPositionQuery, PositionTypesclient=NHLClient(debug=True)
filters= [
GameTypeQuery(game_type="2"),
DraftQuery(year="2020", draft_round="2"),
SeasonQuery(season_start="20202021", season_end="20232024"),
PositionQuery(position=PositionTypes.ALL_FORWARDS)
]
query_builder=QueryBuilder()
query_context: QueryContext=query_builder.build(filters=filters)
data=client.stats.skater_stats_with_query_context(
report_type='summary',
query_context=query_context,
aggregate=True
)Each API request uses an additional query parameter called factCayenneExp. This defaults to gamesPlayed>=1
but can be overridden by setting the fact_query parameter in the QueryContextObject object. These can
be combined together with and to create a more complex query. It supports >, <, >=, <=. For example: shootingPct>=0.01 and timeOnIcePerGame>=60 and faceoffWinPct>=0.01 and shots>=1
This should support the following filters:
gamesPlayedpointsgoalspointsPerGamepenaltyMinutesplusMinusppGoals# power play goalsevGoals# even strength goalspointsPerGamepenaltyMinutesevPoints# even strength pointsppPoints# power play pointsgameWinningGoalsotGoalsshPoints# short handed pointsshGoals# short handed goalsshootingPcttimeOnIcePerGamefaceoffWinPctshots
query_builder=QueryBuilder()
query_context: QueryContext=query_builder.build(filters=filters)
query_context.fact_query="gamesPlayed>=1 and goals>=10"# defaults to gamesPlayed>=1data=client.stats.skater_stats_with_query_context(
report_type='summary',
query_context=query_context,
aggregate=True
)The QueryContext object will hold the result of the built query with the supplied queries.
In the event of an invalid query (bad data, wrong option, etc), the QueryContext object will
hold all the errors that were encountered during the build process. This should help in debugging.
You can quickly check the QueryContext object for errors by calling query_context.is_valid(). Any "invalid" filters
will be removed from the output query, but anything that is still valid will be included.
...
query_context: QueryContext=query_builder.build(filters=filters)
query_context.is_valid() # False if any of the filters fails its validation checkquery_context.errors- Install Poetry
curl -sSL https://install.python-poetry.org | python3 -
or using pipx
pipx install poetry
poetry install --with devpoetry shell
The build pipeline will run black, ruff, and pytest. Please make sure these are passing before submitting a PR.
$ poetryshell# You can then run the following
$ pytest
$ ruff .
$ black .poetry build
poetry publish -r test-pypi
# View current version
poetry version
# Bump version
poetry version patch # 0.1.0 -> 0.1.1
poetry version minor # 0.1.0 -> 0.2.0
poetry version major # 0.1.0 -> 1.0.0
# Set specific version
poetry version 2.0.0
# Set pre-release versions
poetry version prepatch # 0.1.0 -> 0.1.1-alpha.0
poetry version preminor # 0.1.0 -> 0.2.0-alpha.0
poetry version premajor # 0.1.0 -> 1.0.0-alpha.0
# Specify pre-release identifier
poetry version prerelease # 0.1.0 -> 0.1.0-alpha.0
poetry version prerelease beta # 0.1.0-alpha.0 -> 0.1.0-beta.0