Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,37 @@ The MLB Stats API publishes the available values directly:

Common stat groups include `hitting`, `pitching`, and `fielding`. Available stat types depend on the group and endpoint. Examples include `season`, `career`, `seasonAdvanced`, `gameLog`, and `playLog`.

## Numeric stat fields are typed as `float`

Rate and average stats such as `avg`, `obp`, `slg`, `ops`, `era`, `whip`, and
`babip` are typed as `Optional[float]`. The MLB Stats API returns these as
decimal strings (for example `".287"`), and Pydantic converts them to floats
automatically:

```python
split.stat.avg == 0.287 # not ".287"
split.stat.model_dump()["avg"] # 0.287, not ".287"
```

This is a behavioral change from earlier releases where these fields were
`str`. Code relying on string operations (`avg.startswith(".")`) or on
`avg == ".287"` needs to switch to numeric comparisons.

The MLB Stats API also uses two placeholder strings, `".---"` and `"-.--"`,
for these rate stats when the underlying value is not applicable (for
example a caught-stealing percentage when nobody has attempted a steal).
These two known sentinels are normalized to `None` before conversion, so
`split.stat.avg` is `None` rather than raising a validation error. Any other
non-numeric string still raises a `ValidationError`, since it isn't a
sentinel MLB is known to send.

Fields that use MLB's innings notation remain `str`, because values like
`"6.2"` mean 6 2/3 innings rather than the decimal 6.2:

- `SimpleFieldingSplit.innings`
- `SimplePitchingSplit.innings_pitched`
- `AdvancedPitchingSplit.innings_pitched_per_game`

## Related documentation

- [Method reference](methods.md)
Expand Down
45 changes: 30 additions & 15 deletions mlbstatsapi/models/stats/catching.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import Optional, List, ClassVar
from pydantic import Field
from pydantic import Field, field_validator
from mlbstatsapi.models.base import MLBBaseModel
from mlbstatsapi.models.teams import Team
from mlbstatsapi.models.game import Game
from .stats import Split
from .sentinels import normalize_mlb_float_sentinel


class SimpleCatchingSplit(MLBBaseModel):
Expand All @@ -30,23 +31,23 @@ class SimpleCatchingSplit(MLBBaseModel):
The number of hits while catching.
hit_by_pitch : int
The number of batters hit by a pitch while catching.
avg : str
avg : float
The batting average while catching.
at_bats : int
The number of at bats while catching.
obp : str
obp : float
The on base percentage while catching.
slg : str
slg : float
The slugging percentage while catching.
ops : str
ops : float
The on-base slugging while catching.
caught_stealing : int
The number of runners caught stealing by the catcher.
caught_stealing_percentage : str
caught_stealing_percentage : float
Percentage of runners caught stealing by the catcher.
stolen_bases : int
The number of stolen bases while catching.
stolen_base_percentage : str
stolen_base_percentage : float
The stolen base percentage against the catcher.
earned_runs : int
The earned run amount against the catcher.
Expand All @@ -62,7 +63,7 @@ class SimpleCatchingSplit(MLBBaseModel):
The number of pick offs while catching.
total_bases : int
The total number of bases.
strikeout_walk_ratio : str
strikeout_walk_ratio : float
The strike out to walk ratio while catching.
catchers_interference : int
The number of times catcher interference committed.
Expand All @@ -84,29 +85,43 @@ class SimpleCatchingSplit(MLBBaseModel):
intentional_walks: Optional[int] = Field(default=None, alias="intentionalWalks")
hits: Optional[int] = None
hit_by_pitch: Optional[int] = Field(default=None, alias="hitByPitch")
avg: Optional[str] = None
avg: Optional[float] = None
at_bats: Optional[int] = Field(default=None, alias="atBats")
obp: Optional[str] = None
slg: Optional[str] = None
ops: Optional[str] = None
obp: Optional[float] = None
slg: Optional[float] = None
ops: Optional[float] = None
caught_stealing: Optional[int] = Field(default=None, alias="caughtStealing")
caught_stealing_percentage: Optional[str] = Field(default=None, alias="caughtStealingPercentage")
caught_stealing_percentage: Optional[float] = Field(default=None, alias="caughtStealingPercentage")
stolen_bases: Optional[int] = Field(default=None, alias="stolenBases")
stolen_base_percentage: Optional[str] = Field(default=None, alias="stolenBasePercentage")
stolen_base_percentage: Optional[float] = Field(default=None, alias="stolenBasePercentage")
earned_runs: Optional[int] = Field(default=None, alias="earnedRuns")
batters_faced: Optional[int] = Field(default=None, alias="battersFaced")
games_pitched: Optional[int] = Field(default=None, alias="gamesPitched")
hit_batsmen: Optional[int] = Field(default=None, alias="hitBatsmen")
wild_pitches: Optional[int] = Field(default=None, alias="wildPitches")
pickoffs: Optional[int] = None
total_bases: Optional[int] = Field(default=None, alias="totalBases")
strikeout_walk_ratio: Optional[str] = Field(default=None, alias="strikeoutWalkRatio")
strikeout_walk_ratio: Optional[float] = Field(default=None, alias="strikeoutWalkRatio")
catchers_interference: Optional[int] = Field(default=None, alias="catchersInterference")
sac_bunts: Optional[int] = Field(default=None, alias="sacBunts")
sac_flies: Optional[int] = Field(default=None, alias="sacFlies")
passed_ball: Optional[int] = Field(default=None, alias="passedBall")
pickoff_attempts: Optional[int] = Field(default=None, alias="pickoffAttempts")

@field_validator(
"avg",
"obp",
"slg",
"ops",
"caught_stealing_percentage",
"stolen_base_percentage",
"strikeout_walk_ratio",
mode="before",
)
@classmethod
def normalize_float_sentinels(cls, value):
return normalize_mlb_float_sentinel(value)


class CatchingSeason(Split):
"""
Expand Down
41 changes: 28 additions & 13 deletions mlbstatsapi/models/stats/fielding.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from mlbstatsapi.models.teams import Team
from mlbstatsapi.models.game import Game
from .stats import Split
from .sentinels import normalize_mlb_float_sentinel


class SimpleFieldingSplit(MLBBaseModel):
Expand All @@ -21,11 +22,11 @@ class SimpleFieldingSplit(MLBBaseModel):
The number of games started.
caught_stealing : int
The number of runners caught stealing.
caught_stealing_percentage : str
caught_stealing_percentage : float
The percentage of runners caught stealing.
stolen_bases : int
The number of stolen bases.
stolen_base_percentage : str
stolen_base_percentage : float
The stolen base percentage.
assists : int
The number of assists.
Expand All @@ -35,14 +36,15 @@ class SimpleFieldingSplit(MLBBaseModel):
The number of errors committed.
chances : int
The number of chances.
fielding : str
fielding : float
The fielding percentage.
range_factor_per_game : str
range_factor_per_game : float
Range rating per game.
range_factor_per_9_inn : str
range_factor_per_9_inn : float
Range factor per 9 innings.
innings : str
The number of innings played.
The number of innings played. Represented as MLB innings notation
(e.g. "6.2" means 6 2/3 innings), not a true decimal value.
games : int
The number of games played.
passed_ball : int
Expand All @@ -51,7 +53,7 @@ class SimpleFieldingSplit(MLBBaseModel):
The number of double plays.
triple_plays : int
The number of triple plays.
catcher_era : str
catcher_era : float
The catcher ERA of the fielding stat.
catchers_interference : int
The number of times catchers interference was committed.
Expand All @@ -67,22 +69,22 @@ class SimpleFieldingSplit(MLBBaseModel):
games_played: Optional[int] = Field(default=None, alias="gamesPlayed")
games_started: Optional[int] = Field(default=None, alias="gamesStarted")
caught_stealing: Optional[int] = Field(default=None, alias="caughtStealing")
caught_stealing_percentage: Optional[str] = Field(default=None, alias="caughtStealingPercentage")
caught_stealing_percentage: Optional[float] = Field(default=None, alias="caughtStealingPercentage")
stolen_bases: Optional[int] = Field(default=None, alias="stolenBases")
stolen_base_percentage: Optional[str] = Field(default=None, alias="stolenBasePercentage")
stolen_base_percentage: Optional[float] = Field(default=None, alias="stolenBasePercentage")
assists: Optional[int] = None
putouts: Optional[int] = None
errors: Optional[int] = None
chances: Optional[int] = None
fielding: Optional[str] = None
range_factor_per_game: Optional[str] = Field(default=None, alias="rangeFactorPerGame")
range_factor_per_9_inn: Optional[str] = Field(default=None, alias="rangeFactorPer9Inn")
fielding: Optional[float] = None
range_factor_per_game: Optional[float] = Field(default=None, alias="rangeFactorPerGame")
range_factor_per_9_inn: Optional[float] = Field(default=None, alias="rangeFactorPer9Inn")
innings: Optional[str] = None
games: Optional[int] = None
passed_ball: Optional[int] = Field(default=None, alias="passedBall")
double_plays: Optional[int] = Field(default=None, alias="doublePlays")
triple_plays: Optional[int] = Field(default=None, alias="triplePlays")
catcher_era: Optional[str] = Field(default=None, alias="catcherEra")
catcher_era: Optional[float] = Field(default=None, alias="catcherEra")
catchers_interference: Optional[int] = Field(default=None, alias="catchersInterference")
wild_pitches: Optional[int] = Field(default=None, alias="wildPitches")
throwing_errors: Optional[int] = Field(default=None, alias="throwingErrors")
Expand All @@ -96,6 +98,19 @@ def empty_dict_to_none(cls, v: Any) -> Any:
return None
return v

@field_validator(
"caught_stealing_percentage",
"stolen_base_percentage",
"fielding",
"range_factor_per_game",
"range_factor_per_9_inn",
"catcher_era",
mode="before",
)
@classmethod
def normalize_float_sentinels(cls, value: Any) -> Any:
return normalize_mlb_float_sentinel(value)


class FieldingSeasonAdvanced(Split):
"""
Expand Down
Loading
Loading