Skip to main content

Baseball statistics and MLB data for Python: sabermetrics calculations plus a zero-dependency client for the MLB Stats API.

Project description

⚾ pyhomerun

Baseball statistics and MLB data for Python — with zero dependencies.

PyPI License: MIT Python 3.9+

pyhomerun does five things:

  1. Sabermetrics — pure functions and stat-line dataclasses for batting, pitching, fielding, and team statistics (AVG, OBP, SLG, OPS, wOBA, wRC+, ERA, FIP, xFIP, Pythagorean win expectation, ...), plus situational run-expectancy (RE24). Plain numbers in, plain numbers out.
  2. MLB dataMLBClient, a tiny client for the free, key-less MLB Stats API (players, season stats, teams, rosters, schedules, standings, boxscores, play-by-play), with optional disk caching, retry with backoff, and typo-tolerant player lookup.
  3. CSV/dict/array exportto_csv(), to_records()/to_dict(), and to_numpy()/to_dataframe() turn a collection of stat lines (or Statcast rows) into CSV, plain Python data, or a numpy array/pandas DataFrame — no third-party library required unless you reach for the last two.
  4. A terminal commandpyhomerun standings, pyhomerun scores, pyhomerun player "...", pyhomerun export ... for a quick look without writing any Python.

It is built entirely on the Python standard library — installing it installs nothing else, and the test suite runs with stock Python. to_numpy()/to_dataframe() are the one exception, and even those only import numpy/import pandas lazily, inside the function body, when you actually call them.

Installation

Available on PyPI: https://pypi.org/project/pyhomerun/

pip install pyhomerun

Or straight from source:

git clone https://github.com/SRock44/pyhomerun
cd pyhomerun
pip install .

Requires Python 3.9+. If you want to_numpy()/to_dataframe(), install the matching extra (or already have numpy/pandas around — pyhomerun doesn't care how they got there):

pip install pyhomerun[numpy]
pip install pyhomerun[pandas]

Quick start

Calculate statistics

import pyhomerun as bb

# Classic rate stats
avg = bb.batting_average(hits=200, at_bats=600)                     # 0.333
obp = bb.on_base_percentage(hits=200, walks=70, hit_by_pitch=5,
                            at_bats=600, sacrifice_flies=5)         # 0.404
tb  = bb.total_bases(hits=200, doubles=40, triples=5, home_runs=35) # 355
slg = bb.slugging_percentage(tb, at_bats=600)                       # 0.592
print(f"{avg:.3f}/{obp:.3f}/{slg:.3f}  OPS {bb.ops(obp, slg):.3f}")

# Advanced stats
bb.woba(walks=70, hit_by_pitch=5, singles=120, doubles=40, triples=5,
        home_runs=35, at_bats=600, sacrifice_flies=5)               # ~0.416
bb.fip(home_runs=18, walks=45, hit_by_pitch=5, strikeouts=190,
       innings_pitched=180)                                         # ~3.19
bb.era(earned_runs=3, innings_pitched=bb.innings(6.2))              # 4.05

Fetch MLB data

from pyhomerun import MLBClient

mlb = MLBClient()

# Find a player and pull their season batting line
judge = mlb.search_players("Aaron Judge")[0]
splits = mlb.player_stats(judge["id"], group="hitting", season=2025)
line = splits[0]["stat"]
print(judge["fullName"], line["avg"], line["homeRuns"], line["ops"])

# Today's games
for game in mlb.schedule():
    away, home = game["teams"]["away"], game["teams"]["home"]
    print(f'{away["team"]["name"]} at {home["team"]["name"]}{game["status"]["detailedState"]}')

# Standings
for division in mlb.standings(season=2025):
    for record in division["teamRecords"]:
        print(record["team"]["name"], record["wins"], record["losses"])

Put them together: stat lines

BattingLine and PitchingLine bundle counting stats and expose every derived stat — and they build straight from API responses, so there's no field-mapping glue:

import pyhomerun as bb

mlb = bb.MLBClient()
player = mlb.search_players("Juan Soto")[0]
split = mlb.player_stats(player["id"], group="hitting", season=2025)[0]

line = bb.BattingLine.from_mlb(split)
print(player["fullName"], line.slash())      # 0.266/0.396/0.525
print(f"wOBA {line.woba():.3f}  wRC+ {line.wrc_plus():.0f}  BABIP {line.babip:.3f}")

Lines add together, so combining splits or seasons is just +:

career = bb.BattingLine()
for split in mlb.player_stats(player["id"], group="hitting", stat_type="yearByYear"):
    career += bb.BattingLine.from_mlb(split)
print(career.slash(), career.home_runs)

You can also build lines by hand — every field defaults to 0:

line = bb.BattingLine(at_bats=550, hits=150, doubles=30, triples=5, home_runs=25,
                      walks=70, hit_by_pitch=5, strikeouts=120, sacrifice_flies=5)
line.ops                       # 0.839...
line.wraa()                    # runs above average

arm = bb.PitchingLine(outs=540, hits=160, earned_runs=65, walks=50,
                      strikeouts=190, home_runs=18, hit_by_pitch=5)
arm.era, arm.whip, arm.fip()   # (3.25, 1.16..., 3.27...)

Team math

bb.pythagorean_expectation(800, 700)      # 0.566 expected win pct
bb.expected_wins(800, 700, games=162)     # 91.2 (Pythagenpat exponent)
bb.magic_number(leader_wins=90, second_place_losses=60)   # 13

Situational stats: run expectancy

from pyhomerun import BaseOutState, run_expectancy, run_value

# Runner on second, one out: how many runs does a team expect to score?
run_expectancy(BaseOutState(on_second=True, outs=1))       # 0.644

# A walk with the bases empty and nobody out — its run value:
before = BaseOutState()
after = BaseOutState(on_first=True)
run_value(before, after, runs_scored=0)                    # 0.37

CSV export

from pyhomerun import to_csv

roster_lines = {"Aaron Judge": line, "Juan Soto": other_line}  # name -> BattingLine
to_csv(roster_lines)                        # CSV text
with open("roster.csv", "w", newline="") as f:
    to_csv(roster_lines, file=f)

Bulk export: dicts, numpy, pandas

to_records()/to_dict() flatten stat lines (or already record-shaped data, like StatcastClient.search() output) into plain Python — no pyhomerun objects left in the result:

from pyhomerun import BattingLine, to_records, to_dict

roster_lines = {"Aaron Judge": line, "Juan Soto": other_line}  # name -> BattingLine

to_records(roster_lines)                  # [{'name': 'Aaron Judge', 'at_bats': 550, ..., 'avg': 0.327}, ...]
to_dict(roster_lines)                     # {'name': [...], 'at_bats': [...], ..., 'avg': [...]} - columnar
to_dict(roster_lines, records=True)       # same shape as to_records()

to_dict()'s columnar shape is exactly what pandas.DataFrame(...) accepts, so to_numpy()/to_dataframe() are thin convenience wrappers around it — they import numpy/import pandas lazily, only inside the function body when called, so pyhomerun itself never depends on either:

from pyhomerun import to_numpy, to_dataframe

arr = to_numpy(roster_lines)              # numpy structured array; arr["home_runs"]
df = to_dataframe(roster_lines)           # pandas DataFrame, one row per line

# Works on Statcast rows too - no BattingLine/PitchingLine required
from pyhomerun import StatcastClient
savant = StatcastClient()
pitches = savant.search("2024-06-01", "2024-06-30", player_id=660271)
to_dataframe(pitches)                     # one row per pitch

Calling to_numpy()/to_dataframe() without numpy/pandas installed raises a plain ImportError telling you which package to pip install.

From the terminal

Installing the package also installs a pyhomerun command:

pyhomerun standings
pyhomerun scores 2025-10-01
pyhomerun player "Arron Judge"      # fuzzy: finds Aaron Judge despite the typo
pyhomerun teams
pyhomerun roster yankees
pyhomerun export hitting yankees --out yankees.csv

Responses are cached on disk for 5 minutes so re-running commands is instant and doesn't hammer the API. Same thing works as python -m pyhomerun ... if you'd rather not rely on the installed script being on PATH.

API reference

Every function has a full docstring with its formula and a worked example (help(bb.woba)).

Batting

Function Statistic
batting_average(h, ab) AVG
on_base_percentage(h, bb, hbp, ab, sf) OBP
total_bases(h, 2b, 3b, hr) TB
slugging_percentage(tb, ab) SLG
ops(obp, slg) OPS
ops_plus(obp, slg, lg_obp, lg_slg) OPS+ (100 = league average)
isolated_power(slg, avg) ISO
babip(h, hr, ab, k, sf) BABIP
woba(...) wOBA (customizable linear weights)
wraa(woba, pa) wRAA (runs above average)
wrc(woba, pa) / wrc_plus(woba, park_factor) wRC / wRC+ (100 = league average)
runs_created(h, bb, tb, ab) Runs Created (Bill James)
plate_appearances(...) PA
walk_rate(bb, pa) / strikeout_rate(k, pa) BB% / K%
stolen_base_percentage(sb, cs) SB%

Pitching

Function Statistic
innings(6.2) / innings_from_outs(20) Box-score notation → true innings
era(er, ip) ERA
era_plus(era, lg_era) / era_minus(era, lg_era) ERA+ / ERA- (100 = league average)
whip(bb, h, ip) WHIP
fip(hr, bb, hbp, k, ip) FIP (customizable constant)
xfip(fb, bb, hbp, k, ip) xFIP (league HR/FB rate)
k_per_9 / bb_per_9 / hr_per_9 / h_per_9 Per-9 rates
k_bb_ratio(k, bb) K/BB
left_on_base_percentage(...) LOB%
game_score(...) Bill James Game Score

Fielding

Function Statistic
fielding_percentage(po, a, e) FPCT
range_factor_per_game(po, a, g) / range_factor_per_9(po, a, inn) RF/G, RF/9
caught_stealing_percentage(cs, sb) CS%

Team

Function Statistic
run_differential(rs, ra) Run differential
pythagorean_expectation(rs, ra, exponent=2) Pythagorean win %
pythagenpat_exponent(rs, ra, g) Environment-aware exponent
expected_wins(rs, ra, g) Expected win total (Pythagenpat)
magic_number(leader_wins, second_losses) Clinch magic number

Situational

Function Statistic
run_expectancy(BaseOutState(...)) RE24: expected rest-of-inning runs for a base-out state
run_value(before, after, runs_scored) Run value of a play (change in expectancy + runs scored)
RE24_TABLE The underlying {BaseOutState: float} matrix — swap in your own for exact per-season values

Stat lines

Class What it does
BattingLine Counting stats in; avg, obp, slg, ops, iso, babip, walk_rate, ... as properties, plus woba(), wraa(), wrc(), wrc_plus(), runs_created(), slash()
PitchingLine Stores innings as outs for exact addition; era, whip, k_per_9, lob%, ... as properties, plus fip(), era_plus(), era_minus()
*.from_mlb(split) Build either line directly from an MLBClient.player_stats() split
line + line Combine splits/seasons field-by-field
to_csv(lines, file=None) Export a mapping or iterable of lines to CSV (returns text, or writes to file)
to_records(lines) Flatten lines (or dict-like rows, e.g. Statcast) into a list of plain dict
to_dict(lines, records=False) Columnar {column: [values]} by default (ready for pandas.DataFrame(...)), or records=True for the to_records() shape
to_numpy(lines, dtype=None) Numpy structured array — requires numpy (pip install pyhomerun[numpy]), imported lazily
to_dataframe(lines) Pandas DataFrame — requires pandas (pip install pyhomerun[pandas]), imported lazily

MLB Stats API client

Method Returns
MLBClient(timeout=10.0, cache_ttl=None, cache_dir=None, retries=0, backoff_factor=0.5) — pass cache_ttl (seconds) to cache responses on disk, retries for transient-failure retry with backoff
.search_players(name) Player matches (with MLBAM id)
.find_player(name) Best-match player, tolerant of a typo in one name part (see docstring for what it can/can't fix)
.player(player_id) Bio for one player
.player_stats(id, group, stat_type, season) Stat splits ("hitting"/"pitching"/"fielding"; "season"/"career"/"yearByYear"/"gameLog")
.teams(season) / .roster(team_id) Teams / active roster
.schedule(date, team_id) Games for a date (default today)
.standings(season) Division standings
.boxscore(game_pk) / .linescore(game_pk) Game details
.play_by_play(game_pk) Every play of a game, in order
.venues() All MLB ballparks
.awards() / .award_recipients(award_id, season) Awards / winners of one award
.draft(year) Every pick of the amateur draft for a year
.get(path, **params) Any other endpoint, as parsed JSON

All methods return plain dicts/lists parsed from the API's JSON — nothing is hidden, and .get() is an escape hatch to the API's many other endpoints. Errors raise pyhomerun.MLBAPIError.

Conventions

  • Division by zero never raises: rate stats return 0.0 (or math.inf for ERA-style stats when runs scored without an out recorded). See each module's docstring.
  • Innings must be true innings (6⅔, not the box-score 6.2) — convert with innings().
  • League constants: woba and fip ship with representative modern-era defaults. For season-exact work, pass your own WobaWeights / FIP constant using values from the free FanGraphs Guts! page.
  • Typed: the package ships a py.typed marker; all functions are annotated.

Running the tests

No test framework needed:

python -m unittest discover tests -v

(The suite also works under pytest if you prefer it.) The MLB client tests are fully offline — they never touch the network — and every docstring example runs as a doctest.

Roadmap

See ROADMAP.md for what's planned — a Monte Carlo season/playoff simulator next, then a 1.0.0 that promotes Statcast/play-by-play to first-class and closes out remaining coverage gaps before the public API locks in. All still zero required third-party dependencies (to_numpy()/to_dataframe() are opt-in extras, never a hard install). That constraint is the project's core mission, not a starting default.

Contributing

Contributions are welcome! See CONTRIBUTING.md. The short version: keep it dependency-free, add a docstring with formula + example to every public function, and include tests.

License and data

Code is MIT licensed. Data from the MLB Stats API is subject to the MLB copyright notice; this project is not affiliated with or endorsed by MLB.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyhomerun-0.6.0.tar.gz (50.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyhomerun-0.6.0-py3-none-any.whl (38.8 kB view details)

Uploaded Python 3

File details

Details for the file pyhomerun-0.6.0.tar.gz.

File metadata

  • Download URL: pyhomerun-0.6.0.tar.gz
  • Upload date:
  • Size: 50.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pyhomerun-0.6.0.tar.gz
Algorithm Hash digest
SHA256 145ea5fc6f0ce88488b7fc8a09a25100113313e39592f642ec22df681e2b3591
MD5 b3ff415e33353f708c4eb66e2c31607a
BLAKE2b-256 0012180977c2372c7114d0749d4cc931b2488685320115f9b2a0e07d89f1fcea

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyhomerun-0.6.0.tar.gz:

Publisher: publish.yml on SRock44/pyhomerun

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyhomerun-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: pyhomerun-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 38.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pyhomerun-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 89fbb890816f0316851cc3e3d7d46204f2859703f355dc9a0880520e88c9e7ec
MD5 16c4e97c0bcaeea720c6f5e0a72e41c2
BLAKE2b-256 04d551555dde4e3174195b72eec1860365553c9df81d1c6d62a1fb728fab86e0

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyhomerun-0.6.0-py3-none-any.whl:

Publisher: publish.yml on SRock44/pyhomerun

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page