diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 73cc389..aaeacb1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,11 +19,60 @@ Pull requests are the best way to propose changes to the codebase. We actively w
4. Ensure the test suite passes.
5. Issue that pull request!
+## Development
+
+Install dependencies:
+
+```bash
+poetry install -E async
+```
+
+Offline tests are deterministic and should run before every pull request:
+
+```bash
+poetry run pytest \
+ tests/ \
+ --ignore=tests/external_tests
+```
+
+External tests contact the live MLB API. They require internet access and are separate from normal offline CI:
+
+```bash
+poetry run pytest \
+ tests/external_tests/
+```
+
+These live tests may fail because the MLB service is unavailable or because MLB changes undocumented payloads.
+
+Full local validation:
+
+```bash
+poetry run pytest tests/
+rm -rf dist
+poetry build
+python3 scripts/validate_release.py
+poetry run twine check dist/*
+```
+
+`scripts/validate_release.py` is the same release check offline CI runs. It inspects the built wheel and source distribution, clean-installs each artifact into its own temporary virtual environment, and runs the same public-API smoke test against both installed artifacts. Every response it observes comes from injected fake HTTP clients, so it never contacts the MLB API.
+
+Offline CI is the normal pull-request gate. External tests are available manually, on a weekly schedule, and before releases.
+
+## Pull Request Guidelines
+
+- Run offline tests before submitting a PR
+- Use the [PR template](.github/pull_request_template.md) when creating your pull request
+- Follow the branch naming convention:
+ - `feat/` - New features
+ - `fix/` - Bug fixes
+ - `docs/` - Documentation updates
+ - `refactor/` - Code improvements
+
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](https://github.com/zero-sum-seattle/python-mlb-statsapi/issues)
-We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
+We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/zero-sum-seattle/python-mlb-statsapi/issues/new).
## Write bug reports with detail, background, and sample code
**Great Bug Reports** tend to have:
@@ -37,7 +86,7 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue]
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
## Use a Consistent Coding Style
-* Adhere to this projects coding style
+* Adhere to this project's coding style
## License
-By contributing, you agree that your contributions will be licensed under its MIT License.
\ No newline at end of file
+By contributing, you agree that your contributions will be licensed under its MIT License.
diff --git a/README.md b/README.md
index 141379d..2ea6568 100644
--- a/README.md
+++ b/README.md
@@ -9,833 +9,278 @@


-
+### [Wiki](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki) | [Methods](docs/methods.md) | [Examples](docs/examples.md) | [Stats](docs/stats.md) | [Async](docs/async.md) | [Public API](docs/public-api.md) | [MLB Stats API](https://statsapi.mlb.com/)
-### *Copyright Notice*
-This package and its authors are not affiliated with MLB or any MLB team. This API wrapper interfaces with MLB's Stats API. Use of MLB data is subject to the notice posted at http://gdx.mlb.com/components/copyright.txt.
-
-###### This is an educational project - Not for commercial use.
+
+`python-mlb-statsapi` provides Python access to the MLB Stats API for teams, players, schedules, games, stats, and more.
-
-
-## Getting Started
+Returned objects are built with [Pydantic](https://docs.pydantic.dev/), and model fields use Python `snake_case` names.
-*Python-mlb-statsapi* is a Python library that provides access to the MLB Stats API, allowing developers to retrieve information related to MLB teams, players, stats, and more. Written in Python 3.10+.
+Version 1.1 adds first-class async support through `AsyncMlb` while keeping the existing synchronous `Mlb` API available without code changes for sync users.
-All models are built with [Pydantic](https://docs.pydantic.dev/) for robust data validation and serialization. Field names follow Python's `snake_case` convention for a more Pythonic experience.
+### Copyright Notice
-For detailed documentation, check out the [Wiki](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki) which contains information on return objects, endpoint structure, usage examples, and more.
+This package and its authors are not affiliated with MLB or any MLB team. This API wrapper interfaces with MLB's Stats API. Use of MLB data is subject to the notice posted at http://gdx.mlb.com/components/copyright.txt.
+###### This is an educational project - Not for commercial use.
-
+
-### [Examples](#examples) | [Wiki](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki) | [API](https://statsapi.mlb.com/)
+## Installation
-
+### Synchronous client
-## Installation
```bash
python3 -m pip install python-mlb-statsapi
```
-### Python support
-
-| Claim | Value |
-| --- | --- |
-| Minimum declared Python version (`Requires-Python`) | `>=3.10` |
-| CI-validated versions | 3.10, 3.11, 3.12, 3.13, 3.14 |
-
-The minimum declared Python version is 3.10 and the CI-validated versions are
-3.10 through 3.14. There is no upper Python bound. Prerelease interpreters are
-excluded from the required test matrix and are not claimed as supported.
-
-## Quick Start
-```python
->>> import mlbstatsapi
->>> mlb = mlbstatsapi.Mlb()
-
->>> mlb.get_people_id("Ty France")
-[664034]
-
->>> player = mlb.get_person(664034)
->>> print(player.full_name)
-Ty France
-
->>> stats = ['season', 'seasonAdvanced']
->>> groups = ['hitting']
->>> params = {'season': 2022}
->>> mlb.get_player_stats(664034, stats, groups, **params)
-{'hitting': {'season': Stat, 'seasonAdvanced': Stat }}
-
->>> mlb.get_team_id("Seattle Mariners")
-[136]
-
->>> team = mlb.get_team(136)
->>> print(team.name, team.franchise_name)
-Seattle Mariners Seattle
-```
-
-## HTTP Sessions, Timeouts, Retries, and Error Behavior
-
-Version 0.8.0 added shared HTTP Sessions, explicit timeouts, optional Session injection, bounded retries, and structured transport exceptions. Version 0.9.0 made that transport configurable with a public retry policy, richer `MlbHttpError` context, compatibility warnings, and a versioned User-Agent. Version 1.0.0 makes strict HTTP handling the default and documents the stable public API contract.
-
-The `Mlb` client remains synchronous. Shared Sessions pool reusable connections; they do not cache MLB response bodies, and the client does not enable response caching by default.
-
-For the complete reference see the [HTTP transport documentation](docs/http-transport.md). For what changed in this release see the [1.0.0 release notes](docs/releases/1.0.0.md). For the stable public API boundary see the [public API contract](docs/public-api.md).
+### Async support
-### Upgrading to version 1.0
+Install the optional `async` extra to use `AsyncMlb` and `AsyncMlbDataAdapter`:
-`Mlb()` now uses strict HTTP handling by default. It is equivalent to `Mlb(strict_http=True)`.
-
-```text
-Mlb() now uses strict HTTP handling by default
-Final non-404 4xx responses raise MlbHttpError
-404 keeps endpoint-specific None / [] / {} behavior
-Final 5xx still raises MlbHttpError
-Timeouts still raise MlbTimeoutError
-Transport failures still raise MlbTransportError
-Successful invalid JSON still raises MlbDecodeError
-```
-
-Recommended version 1.0 usage:
-
-```python
-import mlbstatsapi
-
-try:
- with mlbstatsapi.Mlb() as mlb:
- player = mlb.get_person(664034)
-except mlbstatsapi.MlbHttpError as exc:
- print(exc.status_code)
- print(exc.reason)
- print(exc.url)
+```bash
+python3 -m pip install "python-mlb-statsapi[async]"
```
-Temporary compatibility opt-out while migrating:
-
-```python
-import mlbstatsapi
+The async extra installs HTTPX. Python 3.10 or newer is required.
-with mlbstatsapi.Mlb(strict_http=False) as mlb:
- player = mlb.get_person(664034)
-```
+| Claim | Value |
+| --- | --- |
+| Minimum Python version | `>=3.10` |
+| CI-validated versions | Python 3.10 through 3.14 |
-`strict_http=False` is a temporary migration opt-out and an explicit request for historical 0.9 behavior. It is not the recommended long-term 1.0 configuration. See [Migrating from 0.9.x to 1.0](docs/http-transport.md#migrating-from-09x-to-10) for the full process, warning-as-error guidance, and before-and-after examples.
+See [Python support](docs/public-api.md#python-support) for the complete policy.
-### Recommended context-manager usage
+## Quick Start
-Prefer a context manager so library-owned HTTP resources are closed when the block exits, including when the block exits because of an exception:
+### Sync
```python
-import mlbstatsapi
+from mlbstatsapi import Mlb
-with mlbstatsapi.Mlb() as mlb:
+with Mlb() as mlb:
player = mlb.get_person(664034)
team = mlb.get_team(136)
-```
-
-One `Mlb` client uses one shared `requests.Session`. The v1 and v1.1 adapters share that Session, so repeated requests can reuse pooled connections. A Session manages a pool of reusable connections; it is not one permanent network connection.
-
-Callers who do not use a context manager may call `mlb.close()` instead. Repeated `close()` calls are safe. Closing a client only closes a Session the library created; a caller-injected Session is left open for its owner.
-
-### Compatibility mode
-
-Callers who need historical 0.9 empty-result behavior for final non-404 4xx responses can pass `strict_http=False`. That path emits `MlbHttpCompatibilityWarning` exactly once per suppressed final response, does not change 404 handling, and does not suppress final 5xx, timeout, transport, or decode failures.
-
-The category inherits from `FutureWarning`, so it stays visible under default Python warning filters. Applications can promote only this package category to an error:
-
-```python
-import warnings
-import mlbstatsapi
-
-warnings.filterwarnings(
- "error",
- category=mlbstatsapi.MlbHttpCompatibilityWarning,
-)
-```
-
-Filter on `mlbstatsapi.MlbHttpCompatibilityWarning` specifically rather than disabling all warnings or all `FutureWarning` instances, which would also hide unrelated notices from other libraries. Prefer removing `strict_http=False` and catching `MlbHttpError` over permanently ignoring the warning.
-
-### Custom timeouts
-
-Every request uses an explicit timeout. The defaults are:
-```text
-Connection timeout: 3.05 seconds
-Read timeout: 30 seconds
+print(player.full_name)
+print(team.name)
```
-The read timeout is the maximum wait while reading response data. It is not one absolute total duration for the complete request.
-
-Use a scalar to apply the same value to both connect and read phases:
+### Async
```python
-import mlbstatsapi
-
-with mlbstatsapi.Mlb(timeout=10) as mlb:
- player = mlb.get_person(664034)
-```
-
-Or provide separate connection and read timeouts:
-
-```python
-import mlbstatsapi
-
-with mlbstatsapi.Mlb(
- timeout=(5.0, 60.0),
-) as mlb:
- player = mlb.get_person(664034)
-```
+import asyncio
-```text
-5.0 seconds: connection timeout
-60.0 seconds: read timeout
-```
+from mlbstatsapi import AsyncMlb
-### Injecting a custom Session
-Advanced callers may inject a caller-owned Session:
+async def main():
+ async with AsyncMlb() as mlb:
+ player = await mlb.get_person(664034)
+ team = await mlb.get_team(136)
-```python
-import requests
-import mlbstatsapi
+ print(player.full_name)
+ print(team.name)
-session = requests.Session()
-session.headers.update({
- "User-Agent": "my-baseball-project/1.0",
-})
-try:
- with mlbstatsapi.Mlb(session=session) as mlb:
- player = mlb.get_person(664034)
-finally:
- session.close()
+asyncio.run(main())
```
-Ownership rules:
+### Without a context manager
-```text
-Library-created Session
- The library configures and closes it
-Caller-injected Session
- The caller configures and closes it
-```
+Context managers are recommended, but both clients can also be created directly. When doing that, close library-owned HTTP resources explicitly.
-`Mlb.close()` does not close a caller-injected Session, and exiting `with Mlb(session=session)` does not close the injected Session either. The library does not replace or reconfigure adapters or headers on an injected Session. Callers control custom retry, TLS, proxy, header, and adapter configuration.
-
-### Reusing the retry policy on a caller-managed Session
-
-`create_retry_policy()` remains public. It returns a new instance of the same tested policy the library mounts on Sessions it creates, so a caller-managed Session can opt in to identical retry behavior:
+#### Sync
```python
-import requests
-import mlbstatsapi
-
-session = requests.Session()
-adapter = requests.adapters.HTTPAdapter(
- max_retries=mlbstatsapi.create_retry_policy(),
-)
-session.mount("https://", adapter)
-session.mount("http://", adapter)
+from mlbstatsapi import Mlb
+mlb = Mlb()
try:
- with mlbstatsapi.Mlb(session=session) as mlb:
- player = mlb.get_person(664034)
-finally:
- session.close()
-```
-
-* The caller mounts the adapters
-* The caller closes the injected Session
-* The library never reconfigures an injected Session
-
-### Versioned User-Agent
-
-A Session created by the library sends a package-specific User-Agent:
+ player = mlb.get_person(664034)
+ team = mlb.get_team(136)
-```text
-python-mlb-statsapi/
+ print(player.full_name)
+ print(team.name)
+finally:
+ mlb.close()
```
-For this release's currently declared package metadata that resolves to `python-mlb-statsapi/1.0.1`. The version is read from the installed distribution metadata, so it always matches the installed release. Only the `User-Agent` header is set; other Requests defaults such as `Accept-Encoding` remain intact, and the header carries no identifiers beyond the package name and version.
-
-Headers on a caller-injected Session are left untouched, so applications that set their own User-Agent keep it.
-
-### Structured exception handling
+#### Async
```python
-import mlbstatsapi
+import asyncio
-try:
- with mlbstatsapi.Mlb() as mlb:
- player = mlb.get_person(664034)
-except mlbstatsapi.MlbTimeoutError:
- print("The MLB API timed out")
-except mlbstatsapi.MlbTransportError:
- print("The request could not reach the MLB API")
-except mlbstatsapi.MlbHttpError as exc:
- print(exc.method)
- print(exc.status_code)
- print(exc.reason)
- print(exc.url)
- print(exc.response_data)
- print(exc.body_excerpt)
-except mlbstatsapi.MlbDecodeError:
- print("The MLB API returned invalid JSON")
-```
+from mlbstatsapi import AsyncMlb
-* `MlbTimeoutError` represents connection and read timeouts
-* `MlbTransportError` represents other request transport failures
-* `MlbHttpError` represents an unexpected final HTTP response
-* `MlbDecodeError` represents invalid JSON in a successful response
-`MlbHttpError` exposes `method`, `status_code`, `reason`, `url`, `response_data`, and `body_excerpt`. `response_data` holds the decoded JSON dictionary or list when the error body contains one, and is `None` otherwise. `body_excerpt` is a bounded excerpt of the response text, capped at 500 characters. Complete response bodies are never automatically logged, and `str(exc)` stays concise.
+async def main():
+ mlb = AsyncMlb()
+ try:
+ player = await mlb.get_person(664034)
+ team = await mlb.get_team(136)
-### Backward-compatible exception handling
+ print(player.full_name)
+ print(team.name)
+ finally:
+ await mlb.aclose()
-All new transport exceptions inherit from `TheMlbStatsApiException`, so existing broad exception handling remains compatible:
-```python
-import mlbstatsapi
-
-try:
- with mlbstatsapi.Mlb() as mlb:
- player = mlb.get_person(664034)
-except mlbstatsapi.TheMlbStatsApiException:
- print("The MLB request failed")
-```
-
-### Default retry behavior
-
-Library-created Sessions automatically retry temporary GET failures for:
-
-```text
-429
-500
-502
-503
-504
-```
-
-```text
-Initial request: 1
-Maximum retries: 3
-Maximum total attempts: 4
-Backoff factor: 0.5
-Retry-After respected: yes
+asyncio.run(main())
```
-Only GET requests are retried, and retries are bounded. Ordinary client errors such as 400, 401, 403, and 404 are not retried. Invalid JSON and Pydantic validation failures are not retried. Retries improve resilience for transient failures, but they do not guarantee success. The retry values are unchanged from versions 0.8.0 and 0.9.0. The version 1.0 strict default does not change retry or Session behavior.
+See [Async usage](docs/async.md) for lifecycle, concurrency, custom HTTPX clients, and the current async endpoint list.
-### Existing 404 compatibility
-
-Version 1.0.0 preserves existing endpoint-specific not-found behavior under both the default and `strict_http=False`. Depending on the endpoint, a 404 may still produce:
-
-```text
-None
-[]
-{}
-```
+## Sync or Async?
-Not every 404 raises `MlbHttpError`, and the strict default does not change that.
+| | `Mlb` | `AsyncMlb` |
+| --- | --- | --- |
+| HTTP library | Requests | HTTPX |
+| Context manager | `with Mlb()` | `async with AsyncMlb()` |
+| Request | `mlb.get_team(...)` | `await mlb.get_team(...)` |
+| Explicit cleanup | `mlb.close()` | `await mlb.aclose()` |
-### HTTP behavior at a glance
+Where an async endpoint is supported, both clients return the same Pydantic models and follow the same public HTTP/error behavior.
-| Final response | Default 1.0 behavior | Explicit compatibility mode |
-| -------------- | -------------------- | --------------------------- |
-| Successful 2xx | Normal result | Normal result |
-| Non-404 4xx | `MlbHttpError` | Warning and historical empty result |
-| 404 | Existing endpoint behavior | Existing endpoint behavior |
-| Final 429 | `MlbHttpError` after retries | Warning and historical empty result after retries |
-| Final 5xx | `MlbHttpError` | `MlbHttpError` |
+The [public API contract](docs/public-api.md#asyncmlb-public-client) is the authoritative list of supported async methods.
-See the [HTTP transport documentation](docs/http-transport.md) for the complete retry policy, Session ownership rules, warning behavior, cleanup behavior, and migration guidance, and the [1.0.0 release notes](docs/releases/1.0.0.md) for the release summary.
+## Concurrent Async Requests
-## Working with Pydantic Models
+`AsyncMlb` supports concurrent requests on the same event loop. Concurrency is controlled by the caller.
-All returned objects are Pydantic models, giving you access to powerful serialization and validation features.
-
-### Convert to Dictionary
```python
->>> player = mlb.get_person(664034)
->>> player.model_dump()
-{'id': 664034, 'full_name': 'Ty France', 'link': '/api/v1/people/664034', ...}
-
-# Exclude None values
->>> player.model_dump(exclude_none=True)
-{'id': 664034, 'full_name': 'Ty France', 'link': '/api/v1/people/664034', ...}
-
-# Include only specific fields
->>> player.model_dump(include={'id', 'full_name', 'primary_position'})
-{'id': 664034, 'full_name': 'Ty France', 'primary_position': Position(...)}
-```
-
-### Convert to JSON
-```python
->>> player = mlb.get_person(664034)
->>> player.model_dump_json()
-'{"id": 664034, "full_name": "Ty France", "link": "/api/v1/people/664034", ...}'
-
-# Pretty print with indentation
->>> print(player.model_dump_json(indent=2))
-{
- "id": 664034,
- "full_name": "Ty France",
- "link": "/api/v1/people/664034",
- ...
-}
-```
-
-### Access Fields with Snake Case Names
-```python
->>> player = mlb.get_person(664034)
->>> player.full_name # Not fullName
-'Ty France'
->>> player.primary_position # Not primaryPosition
-Position(code='3', name='First Base', ...)
->>> player.bat_side # Not batSide
-CodeDesc(code='R', description='Right')
-```
-
-## Documentation
-
-### [People, Person, Players, Coaches](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-People)
-* `Mlb.get_people_id(self, fullname: str, sport_id: int = 1, search_key: str = 'fullname', **params)` - Return Person Id(s) from fullname
-* `Mlb.get_person(self, player_id: int, **params)` - Return Person Object from Id
-* `Mlb.get_people(self, sport_id: int = 1, **params)` - Return all Players from Sport
-### [Draft](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Draft(round))
-* `Mlb.get_draft(self, year_id: int, **params)` - Return a draft for a given year
-### [Awards](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Award)
-* `Mlb.get_awards(self, award_id: int, **params)` - Return award recipients for a given award
-### [Teams](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Team)
-* `Mlb.get_team_id(self, team_name: str, search_key: str = 'name', **params)` - Return Team Id(s) from name
-* `Mlb.get_team(self, team_id: int, **params)` - Return Team Object from Team Id
-* `Mlb.get_teams(self, sport_id: int = 1, **params)` - Return all Teams for Sport
-* `Mlb.get_team_coaches(self, team_id: int, **params)` - Return coaching roster for team for current or specified season
-* `Mlb.get_team_roster(self, team_id: int, **params)` - Return player roster for team for current or specified season
-### [Stats](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Stats)
-* `Mlb.get_player_stats(self, person_id: int, stats: list, groups: list, **params)` - Return stats by player id, stat type and groups
-* `Mlb.get_team_stats(self, team_id: int, stats: list, groups: list, **params)` - Return stats by team id, stat types and groups
-* `Mlb.get_stats(self, stats: list, groups: list, **params: dict)` - Return stats by stat type and group args
-* `Mlb.get_players_stats_for_game(self, person_id: int, game_id: int, **params)` - Return player stats for a game
-### [Gamepace](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Gamepace)
-* `Mlb.get_gamepace(self, season: str, sport_id=1, **params)` - Return pace of game metrics for specific sport, league or team.
-### [Venues](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Venue)
-* `Mlb.get_venue_id(self, venue_name: str, search_key: str = 'name', **params)` - Return Venue Id(s)
-* `Mlb.get_venue(self, venue_id: int, **params)` - Return Venue Object from venue Id
-* `Mlb.get_venues(self, **params)` - Return all Venues
-### [Sports](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Sport)
-* `Mlb.get_sport(self, sport_id: int, **params)` - Return a Sport object from Id
-* `Mlb.get_sports(self, **params)` - Return all Sports
-* `Mlb.get_sport_id(self, sport_name: str, search_key: str = 'name', **params)`- Return Sport Id from name
-### [Schedules](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Schedule)
-* `Mlb.get_schedule(self, date: str, start_date: str, end_date: str, sport_id: int, team_id: int, **params)` - Return a Schedule
-### [Divisions](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Division)
-* `Mlb.get_division(self, division_id: int, **params)` - Return a Division
-* `Mlb.get_divisions(self, **params)` - Return all Divisions
-* `Mlb.get_division_id(self, division_name: str, search_key: str = 'name', **params)` - Return Division Id(s) from name
-### [Leagues](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-League)
-* `Mlb.get_league(self, league_id: int, **params)` - Return a League from Id
-* `Mlb.get_leagues(self, **params)` - Return all Leagues
-* `Mlb.get_league_id(self, league_name: str, search_key: str = 'name', **params)` - Return League Id(s)
-### [Seasons](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Season)
-* `Mlb.get_season(self, season_id: str, sport_id: int = None, **params)` - Return a season
-* `Mlb.get_seasons(self, sportid: int = None, **params)` - Return all seasons
-### [Standings](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Standings)
-* `Mlb.get_standings(self, league_id: int, season: str, **params)` - Return standings
-### [Schedules](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Schedule)
-* `Mlb.get_schedule(self, date: str = None, start_date: str = None, end_date: str = None, sport_id: int = 1, team_id: int = None, **params)` - Return a Schedule from dates
-* `Mlb.get_scheduled_games_by_date(self, date: str = None,start_date: str = None, end_date: str = None, sport_id: int = 1, **params)` - Return game ids from dates
-### [Games](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Game)
-* `Mlb.get_game(self, game_id: int, **params)` - Return the Game for a specific Game Id
-* `Mlb.get_game_play_by_play(self, game_id: int, **params)` - Return Play by play data for a game
-* `Mlb.get_game_line_score(self, game_id: int, **params)` - Return a Linescore for a game
-* `Mlb.get_game_box_score(self, game_id: int, **params)` - Return a Boxscore for a game
+import asyncio
+from mlbstatsapi import AsyncMlb
-## Contributing
-
-Contributions are welcome! Whether it's bug fixes, new features, or documentation improvements, we appreciate your help.
-
-### Getting Started
-
-1. Fork the repository
-2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/python-mlb-statsapi.git`
-3. Install dependencies: `poetry install`
-4. Create a branch: `git checkout -b feat/your-feature`
-### Development
+async def main():
+ async with AsyncMlb() as mlb:
+ player, team = await asyncio.gather(
+ mlb.get_person(664034),
+ mlb.get_team(136),
+ )
-Offline tests are deterministic and should run before every pull request:
-
-```bash
-poetry run pytest \
- tests/ \
- --ignore=tests/external_tests
-```
+ return player, team
-External tests contact the live MLB API. They require internet access and are separate from normal offline CI:
-```bash
-poetry run pytest \
- tests/external_tests/
-```
-
-These live tests may fail because the MLB service is unavailable or because MLB changes undocumented payloads.
-
-Full local validation:
-
-```bash
-poetry run pytest tests/
-rm -rf dist
-poetry build
-python3 scripts/validate_release.py
-poetry run twine check dist/*
+player, team = asyncio.run(main())
```
-`scripts/validate_release.py` is the same release check offline CI runs. It inspects the built wheel and source distribution, clean-installs each artifact into its own temporary virtual environment, and runs the same public-API smoke test against both installed artifacts. The smoke test verifies the declared metadata, the supported package-root imports, the strict HTTP default, explicit strict and compatibility modes, the versioned `User-Agent`, and injected-Session ownership. Every response it observes comes from an injected fake Session, so it never contacts the MLB API.
+`AsyncMlb` does not create hidden background tasks or automatic request fanout.
-Offline CI is the normal pull-request gate. External tests are available manually, on a weekly schedule, and before releases.
+## Common Methods
-### Pull Request Guidelines
+### Players
-- Run offline tests before submitting a PR
-- Use the [PR template](.github/pull_request_template.md) when creating your pull request
-- Follow the branch naming convention:
- - `feat/` - New features
- - `fix/` - Bug fixes
- - `docs/` - Documentation updates
- - `refactor/` - Code improvements
-
-### Reporting Issues
-
-Found a bug or have a feature request? Please [open an issue](https://github.com/zero-sum-seattle/python-mlb-statsapi/issues/new) with:
-
-- A clear description of the problem or feature
-- Steps to reproduce (for bugs)
-- Expected vs actual behavior
-- Python version and package version
-
-
-## Examples
-
-Let's show some examples of getting stat objects from the API. What is baseball without stats, right?
-
-### Player Stats
-Get the Id(s) of the players you want stats for and set stat types and groups.
```python
->>> mlb = mlbstatsapi.Mlb()
->>> player_id = mlb.get_people_id("Ty France")[0]
->>> stats = ['season', 'career']
->>> groups = ['hitting', 'pitching']
->>> params = {'season': 2022}
+player = mlb.get_person(664034)
+players = mlb.get_people()
+player_ids = mlb.get_people_id("Ty France")
```
-Use player id with stat types and groups to return a stats dictionary
-```python
->>> stat_dict = mlb.get_player_stats(player_id, stats=stats, groups=groups, **params)
->>> season_hitting_stat = stat_dict['hitting']['season']
->>> career_pitching_stat = stat_dict['pitching']['career']
-```
+### Teams
-Print season hitting stats using Pydantic's `model_dump()`
```python
->>> for split in season_hitting_stat.splits:
-... print(split.stat.model_dump(exclude_none=True))
-{'games_played': 140, 'groundouts': 163, 'airouts': 148, 'runs': 65, 'doubles': 27, ...}
+team = mlb.get_team(136)
+teams = mlb.get_teams()
+team_ids = mlb.get_team_id("Seattle Mariners")
```
-Or access individual fields directly
-```python
->>> for split in season_hitting_stat.splits:
-... print(f"Games: {split.stat.games_played}")
-... print(f"Home Runs: {split.stat.home_runs}")
-... print(f"Batting Avg: {split.stat.avg}")
-Games: 140
-Home Runs: 20
-Batting Avg: .274
-```
+### Stats
-### Team Stats
-Get the Team Id(s)
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> team_id = mlb.get_team_id('Seattle Mariners')[0]
-```
+The stats API has several entry points and returns a nested `stats[group][type]` structure. See the dedicated [Stats Guide](docs/stats.md) for `get_player_stats()`, `get_team_stats()`, `get_stats()`, and `get_players_stats_for_game()` examples using both `Mlb` and `AsyncMlb`.
-Set the stat types and groups
-```python
->>> stats = ['season', 'seasonAdvanced']
->>> groups = ['hitting']
->>> params = {'season': 2022}
-```
+### Schedule
-Use team id and the stat types and groups to return season hitting stats
```python
->>> stats = mlb.get_team_stats(team_id, stats=stats, groups=groups, **params)
->>> season_hitting = stats['hitting']['season']
->>> advanced_hitting = stats['hitting']['seasonAdvanced']
+schedule = mlb.get_schedule(date="2022-10-13")
```
-Print stats as JSON
-```python
->>> for split in season_hitting.splits:
-... print(split.stat.model_dump_json(indent=2, exclude_none=True))
-{
- "games_played": 162,
- "groundouts": 1273,
- "runs": 690,
- "doubles": 229,
- ...
-}
-```
+`get_schedule` is available on both `Mlb` and `AsyncMlb`.
-### Expected Stats
-```python
->>> player_id = mlb.get_people_id('Ty France')[0]
->>> stats = ['expectedStatistics']
->>> group = ['hitting']
->>> params = {'season': 2022}
-
->>> stats = mlb.get_player_stats(player_id, stats=stats, groups=group, **params)
->>> expected = stats['hitting']['expectedStatistics']
->>> for split in expected.splits:
-... print(f"Expected AVG: {split.stat.avg}")
-... print(f"Expected SLG: {split.stat.slg}")
-Expected AVG: .259
-Expected SLG: .394
-```
+See the [method reference](docs/methods.md) for the full method documentation that previously lived in the README. Longer runnable examples live in [docs/examples.md](docs/examples.md).
-### vsPlayer Stats
-Get pitcher and batter player Ids
-```python
->>> ty_france_id = mlb.get_people_id('Ty France')[0]
->>> shohei_ohtani_id = mlb.get_people_id('Shohei Ohtani')[0]
-```
+## HTTP and Error Behavior
-Set stat type, stat groups, and params
-```python
->>> stats = ['vsPlayer']
->>> group = ['hitting']
->>> params = {'opposingPlayerId': shohei_ohtani_id, 'season': 2022}
-```
+Both clients use explicit timeouts, structured exceptions, and pooled HTTP connections. `strict_http=True` is the default. Final non-404 4xx responses raise `MlbHttpError`, while existing endpoint-specific 404 behavior is preserved.
-Get stats
-```python
->>> stats = mlb.get_player_stats(ty_france_id, stats=stats, groups=group, **params)
->>> vs_player = stats['hitting']['vsPlayer']
->>> for split in vs_player.splits:
-... print(f"Games: {split.stat.games_played}, Hits: {split.stat.hits}")
-Games: 2, Hits: 2
-```
+Library-created clients send a versioned User-Agent. The current package version sends `python-mlb-statsapi/1.0.1`. See the [HTTP transport documentation](docs/http-transport.md) for the full transport contract.
-### Hot/Cold Zones
-```python
->>> ty_france_id = mlb.get_people_id('Ty France')[0]
->>> stats = ['hotColdZones']
->>> hitting_group = ['hitting']
->>> params = {'season': 2022}
-
->>> hotcoldzones = mlb.get_player_stats(ty_france_id, stats=stats, groups=hitting_group, **params)
->>> zones = hotcoldzones['stats']['hotColdZones']
-
->>> for split in zones.splits:
-... print(f"Stat: {split.stat.name}")
-... for zone in split.stat.zones:
-... print(f" Zone {zone.zone}: {zone.value}")
-Stat: battingAverage
- Zone 01: .226
- Zone 02: .400
- ...
-```
+The main transport exceptions are:
-### Schedule Examples
-Get a schedule for a given date
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> schedule = mlb.get_schedule(date='2022-10-13')
->>> dates = schedule.dates
-
->>> for date in dates:
-... for game in date.games:
-... print(f"Game: {game.game_pk}")
-... print(f"Status: {game.status.detailed_state}")
-... print(f"Home: {game.teams.home.team.name}")
-... print(f"Away: {game.teams.away.team.name}")
-```
+* `MlbHttpError`
+* `MlbTimeoutError`
+* `MlbTransportError`
+* `MlbDecodeError`
-### Game Examples
-Get a Game for a given game id
```python
->>> mlb = mlbstatsapi.Mlb()
->>> game = mlb.get_game(662242)
-```
+from mlbstatsapi import Mlb, MlbHttpError, MlbTimeoutError
-Get the weather for a game
-```python
->>> weather = game.game_data.weather
->>> print(f"Condition: {weather.condition}")
->>> print(f"Temperature: {weather.temp}")
->>> print(f"Wind: {weather.wind}")
+try:
+ with Mlb() as mlb:
+ player = mlb.get_person(664034)
+except MlbTimeoutError:
+ print("The MLB API timed out")
+except MlbHttpError as exc:
+ print(exc.status_code, exc.reason)
```
-Get the current status of a game
-```python
->>> linescore = game.live_data.linescore
->>> home_info = game.game_data.teams.home
->>> away_info = game.game_data.teams.away
->>> home_status = linescore.teams.home
->>> away_status = linescore.teams.away
-
->>> print(f"Home: {home_info.franchise_name} {home_info.club_name}")
->>> print(f" Runs: {home_status.runs}, Hits: {home_status.hits}, Errors: {home_status.errors}")
->>> print(f"Away: {away_info.franchise_name} {away_info.club_name}")
->>> print(f" Runs: {away_status.runs}, Hits: {away_status.hits}, Errors: {away_status.errors}")
->>> print(f"Inning: {linescore.inning_half} {linescore.current_inning_ordinal}")
-```
+For timeouts, retries, compatibility mode, ownership rules, and transport details, see [docs/http-transport.md](docs/http-transport.md).
-Get play by play, line score, and box score objects
-```python
->>> play_by_play = game.live_data.plays
->>> line_score = game.live_data.linescore
->>> box_score = game.live_data.boxscore
-```
+## Working with Models
-#### Play by Play
-Get only the play by play for a given game id
-```python
->>> playbyplay = mlb.get_game_play_by_play(662242)
-```
-
-#### Line Score
-Get only the line score for a given game id
-```python
->>> linescore = mlb.get_game_line_score(662242)
-```
+Every returned model object uses Pydantic and Python-style `snake_case` fields:
-#### Box Score
-Get only the box score for a given game id
```python
->>> boxscore = mlb.get_game_box_score(662242)
-```
+from mlbstatsapi import Mlb
-### Gamepace Examples
-Get pace of game metrics for a specific season
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> gamepace = mlb.get_gamepace(season=2021)
->>> print(f"Hits per game: {gamepace.sports[0].sport_game_pace.hits_per_game}")
-```
+with Mlb() as mlb:
+ player = mlb.get_person(664034)
-### People Examples
-Get all Players for a given sport id
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> players = mlb.get_people(sport_id=1)
->>> for player in players:
-... print(f"{player.id}: {player.full_name}")
+print(player.full_name) # not fullName
+print(player.model_dump(exclude_none=True))
+print(player.model_dump_json(indent=2))
```
-Get a player id
-```python
->>> player_id = mlb.get_people_id("Ty France")
->>> print(player_id[0])
-664034
-```
+## Documentation
-### Team Examples
-Get a Team
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> team_id = mlb.get_team_id("Seattle Mariners")[0]
->>> team = mlb.get_team(team_id)
->>> print(f"{team.id}: {team.name}")
->>> print(f"Venue: {team.venue.name}")
-```
+| Document | Contents |
+| --- | --- |
+| [Wiki](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki) | Endpoint reference, return objects, and model documentation |
+| [Method reference](docs/methods.md) | Method signatures and short descriptions from the original README reference |
+| [Usage examples](docs/examples.md) | Extended synchronous examples |
+| [Stats guide](docs/stats.md) | Player, team, general, and per-game stat queries with sync and async examples |
+| [Async usage](docs/async.md) | Async installation, lifecycle, concurrency, and examples |
+| [HTTP transport](docs/http-transport.md) | Timeouts, retries, strict HTTP, exceptions, and ownership |
+| [Public API contract](docs/public-api.md) | Supported symbols, signatures, endpoint methods, and stability policy |
+| [Release notes](docs/releases/) | Release-specific changes and migration notes |
-Get a Player Roster
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> players = mlb.get_team_roster(136)
->>> for player in players:
-... print(f"#{player.jersey_number} {player.person.full_name}")
-```
+## Contributing
-Get a Coach Roster
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> coaches = mlb.get_team_coaches(136)
->>> for coach in coaches:
-... print(f"{coach.person.full_name}: {coach.title}")
-```
+Contributions, bug fixes, tests, and documentation improvements are welcome.
-### Draft Examples
-Get a draft for a year
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> draft = mlb.get_draft('2019')
+```bash
+git clone https://github.com/YOUR_USERNAME/python-mlb-statsapi.git
+cd python-mlb-statsapi
+poetry install -E async
```
-Get Players from Draft
-```python
->>> draftpicks = draft[0].picks
->>> for pick in draftpicks:
-... print(f"Round {pick.pick_round}, Pick {pick.pick_number}: {pick.person.full_name}")
-```
+Run the deterministic offline suite before a pull request:
-### Award Examples
-Get awards for a given award id
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> retired_numbers = mlb.get_awards(award_id='RETIREDUNI_108')
->>> for recipient in retired_numbers.awards:
-... print(f"{recipient.player.full_name}: {recipient.name} ({recipient.date})")
+```bash
+poetry run pytest tests/ --ignore=tests/external_tests
```
-### Venue Examples
-Get a Venue
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> venue_id = mlb.get_venue_id('PNC Park')[0]
->>> venue = mlb.get_venue(venue_id)
->>> print(f"{venue.name} - {venue.location.city}, {venue.location.state}")
-```
+External tests contact the live MLB API and are separate from normal offline CI:
-### Division Examples
-Get a division
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> division = mlb.get_division(200)
->>> print(division.name)
-American League West
+```bash
+poetry run pytest tests/external_tests/
```
-### League Examples
-Get a league
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> league = mlb.get_league(103)
->>> print(league.name)
-American League
-```
+See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development and pull request workflow.
-### Season Examples
-Get a Season
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> season = mlb.get_season(2018)
->>> print(f"Season: {season.season_id}")
->>> print(f"Regular Season: {season.regular_season_start_date} to {season.regular_season_end_date}")
-```
+## License
-### Standings Examples
-Get Standings
-```python
->>> mlb = mlbstatsapi.Mlb()
->>> standings = mlb.get_standings(103, 2018)
->>> for record in standings:
-... print(f"Division: {record.division.name}")
-... for team in record.team_records:
-... print(f" {team.team.name}: {team.wins}-{team.losses}")
-```
+Released under the [MIT License](LICENSE).
diff --git a/docs/async.md b/docs/async.md
new file mode 100644
index 0000000..16af897
--- /dev/null
+++ b/docs/async.md
@@ -0,0 +1,227 @@
+# Async Usage
+
+`AsyncMlb` is the public asynchronous client for `python-mlb-statsapi` 1.1.
+It requires the optional `async` extra.
+
+## Installation
+
+```bash
+python3 -m pip install "python-mlb-statsapi[async]"
+```
+
+A synchronous-only install remains unchanged and does not require HTTPX.
+
+## Quick start
+
+```python
+import asyncio
+
+from mlbstatsapi import AsyncMlb
+
+
+async def main():
+ async with AsyncMlb() as mlb:
+ player = await mlb.get_person(664034)
+ team = await mlb.get_team(136)
+
+ print(player.full_name)
+ print(team.name)
+
+
+asyncio.run(main())
+```
+
+Use `async with` when possible so library-owned HTTP resources are closed when
+the block exits.
+
+## Without a context manager
+
+If a context manager is not practical, create `AsyncMlb` directly and call
+`await mlb.aclose()` when finished:
+
+```python
+import asyncio
+
+from mlbstatsapi import AsyncMlb
+
+
+async def main():
+ mlb = AsyncMlb()
+ try:
+ player = await mlb.get_person(664034)
+ team = await mlb.get_team(136)
+
+ print(player.full_name)
+ print(team.name)
+ finally:
+ await mlb.aclose()
+
+
+asyncio.run(main())
+```
+
+Repeated `aclose()` calls are safe.
+
+## Concurrent requests
+
+One `AsyncMlb` instance supports concurrent in-flight requests on the same
+event loop. Concurrency is controlled by the caller.
+
+```python
+import asyncio
+
+from mlbstatsapi import AsyncMlb
+
+
+async def main():
+ async with AsyncMlb() as mlb:
+ player, team = await asyncio.gather(
+ mlb.get_person(664034),
+ mlb.get_team(136),
+ )
+
+ return player, team
+
+
+player, team = asyncio.run(main())
+```
+
+`AsyncMlb` does not create hidden background tasks or automatic request fanout.
+Cross-event-loop use of the same client is not promised.
+
+## Supported endpoints
+
+The async surface is intentionally smaller than the synchronous `Mlb` surface
+while 1.1 support is being expanded. The currently supported awaitable endpoint
+methods on `release/1.1.0` are:
+
+```text
+get_team(...)
+get_teams(...)
+get_person(...)
+get_people(...)
+get_schedule(...)
+```
+
+Where an async endpoint is supported, it returns the same Pydantic model types
+and follows the same public HTTP/error behavior as the matching synchronous
+method.
+
+For the authoritative list and signatures, see the
+[public API contract](public-api.md#asyncmlb-public-client).
+
+## Error handling
+
+The public exception hierarchy is shared with the synchronous client:
+
+```python
+from mlbstatsapi import (
+ AsyncMlb,
+ MlbDecodeError,
+ MlbHttpError,
+ MlbTimeoutError,
+ MlbTransportError,
+)
+
+
+async def get_player():
+ try:
+ async with AsyncMlb() as mlb:
+ return await mlb.get_person(664034)
+ except MlbTimeoutError:
+ print("The MLB API timed out")
+ except MlbTransportError:
+ print("The request could not reach the MLB API")
+ except MlbHttpError as exc:
+ print(exc.status_code, exc.reason)
+ except MlbDecodeError:
+ print("The MLB API returned invalid JSON")
+```
+
+`strict_http=True` is the default. Existing endpoint-specific 404 behavior is
+preserved. See the [HTTP transport documentation](http-transport.md) for the
+complete status, timeout, retry, and compatibility-mode contract.
+
+## Custom HTTPX client
+
+Advanced callers may inject their own `httpx.AsyncClient`:
+
+```python
+import httpx
+
+from mlbstatsapi import AsyncMlb
+
+
+async def get_person_with_custom_client(client: httpx.AsyncClient, person_id: int):
+ async with AsyncMlb(client=client) as mlb:
+ return await mlb.get_person(person_id)
+```
+
+`async with` and `await` are only valid inside an `async def`, so this is
+written as a plain, reusable function rather than a top-level script. Call it
+however your application already enters async code — `asyncio.run(...)`, a
+web framework's request handler, an existing event loop, and so on. Nothing
+here requires restructuring your application around a `main()` entry point;
+`get_person_with_custom_client()` itself has no opinion on how it is invoked.
+
+Below are a few ways to invoke it, depending on how your application already
+enters async code.
+
+**Script entry point**
+
+```python
+import asyncio
+
+
+async def main():
+ async with httpx.AsyncClient() as client:
+ return await get_person_with_custom_client(client, 664034)
+
+
+asyncio.run(main())
+```
+
+**Inside an application that already runs on an event loop** — a web
+framework's request handler, a worker task, and so on — just `await` it
+directly with a client your application already owns:
+
+```python
+async def handle_request(client: httpx.AsyncClient, person_id: int):
+ return await get_person_with_custom_client(client, person_id)
+```
+
+**FastAPI (or another ASGI framework)**
+
+```python
+from fastapi import FastAPI
+
+app = FastAPI()
+http_client = httpx.AsyncClient()
+
+
+@app.get("/players/{person_id}")
+async def read_player(person_id: int):
+ return await get_person_with_custom_client(http_client, person_id)
+```
+
+**Interactively, with no wrapper at all** — Jupyter/IPython and the
+`python -m asyncio` REPL both support top-level `await`:
+
+```pycon
+>>> import httpx
+>>> client = httpx.AsyncClient()
+>>> player = await get_person_with_custom_client(client, 664034)
+>>> await client.aclose()
+```
+
+An injected client remains caller-owned and is not closed by `AsyncMlb`. In a
+real application the client is typically created once, reused across calls,
+and closed by whatever code owns its lifecycle — the examples above show a
+few ways to run this, not the required shape of your application.
+
+## Documentation boundaries
+
+- [README](../README.md) — installation and quick-start examples
+- [Usage examples](examples.md) — longer synchronous examples
+- [Public API contract](public-api.md) — supported symbols, signatures, and endpoint coverage
+- [HTTP transport](http-transport.md) — timeouts, retries, errors, and compatibility behavior
diff --git a/docs/examples.md b/docs/examples.md
new file mode 100644
index 0000000..442dfb7
--- /dev/null
+++ b/docs/examples.md
@@ -0,0 +1,157 @@
+# Usage Examples
+
+This document collects the longer usage examples that previously lived in the README. The README keeps a short quick start; this guide is the extended tour.
+
+Every example in this file uses the synchronous `Mlb` client. Async usage is documented separately in [async.md](async.md). Stats have their own detailed [stats guide](stats.md) with both sync and async examples.
+
+For return-object structure and endpoint details see the [Wiki](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki). For the supported method list, parameters, and return shapes see the [public API contract](public-api.md). For transport behavior see the [HTTP transport documentation](http-transport.md).
+
+## Without a context manager
+
+A context manager is recommended, but `Mlb` can also be created directly. Call `mlb.close()` when finished so library-owned HTTP resources are released.
+
+```python
+from mlbstatsapi import Mlb
+
+mlb = Mlb()
+try:
+ player = mlb.get_person(664034)
+ team = mlb.get_team(136)
+
+ print(player.full_name)
+ print(team.name)
+finally:
+ mlb.close()
+```
+
+## Working with Pydantic Models
+
+All returned objects are Pydantic models, giving you access to serialization and validation helpers.
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ player = mlb.get_person(664034)
+
+print(player.full_name)
+print(player.model_dump(exclude_none=True))
+print(player.model_dump_json(indent=2))
+```
+
+## Players and teams
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ player_id = mlb.get_people_id("Ty France")[0]
+ team_id = mlb.get_team_id("Seattle Mariners")[0]
+
+ player = mlb.get_person(player_id)
+ team = mlb.get_team(team_id)
+
+print(player.full_name)
+print(team.name)
+```
+
+## Stats
+
+Player, team, general, and per-game stat examples live in the dedicated [Stats Guide](stats.md). It also explains the nested `stats[group][type]` return structure and includes matching `Mlb` and `AsyncMlb` examples.
+
+## Schedule
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ schedule = mlb.get_schedule(date="2022-10-13")
+
+for date in schedule.dates:
+ for game in date.games:
+ print(game.game_pk, game.status.detailed_state)
+```
+
+## Game data
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ game = mlb.get_game(662242)
+ play_by_play = mlb.get_game_play_by_play(662242)
+ line_score = mlb.get_game_line_score(662242)
+ box_score = mlb.get_game_box_score(662242)
+```
+
+## Rosters
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ players = mlb.get_team_roster(136)
+ coaches = mlb.get_team_coaches(136)
+
+for player in players:
+ print(f"#{player.jersey_number} {player.person.full_name}")
+
+for coach in coaches:
+ print(f"{coach.person.full_name}: {coach.title}")
+```
+
+## Draft
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ draft = mlb.get_draft("2019")
+
+for pick in draft[0].picks:
+ print(f"Round {pick.pick_round}, Pick {pick.pick_number}: {pick.person.full_name}")
+```
+
+## Awards
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ retired_numbers = mlb.get_awards(award_id="RETIREDUNI_108")
+
+for recipient in retired_numbers.awards:
+ print(f"{recipient.player.full_name}: {recipient.name} ({recipient.date})")
+```
+
+## Venue, division, league, and season
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ venue_id = mlb.get_venue_id("PNC Park")[0]
+ venue = mlb.get_venue(venue_id)
+ division = mlb.get_division(200)
+ league = mlb.get_league(103)
+ season = mlb.get_season(2018)
+
+print(venue.name)
+print(division.name)
+print(league.name)
+print(season.season_id)
+```
+
+## Standings
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ standings = mlb.get_standings(103, 2018)
+
+for record in standings:
+ print(f"Division: {record.division.name}")
+ for team in record.team_records:
+ print(f" {team.team.name}: {team.wins}-{team.losses}")
+```
diff --git a/docs/methods.md b/docs/methods.md
new file mode 100644
index 0000000..5b26f39
--- /dev/null
+++ b/docs/methods.md
@@ -0,0 +1,222 @@
+# Method Reference
+
+This page contains the method reference that previously lived in the README.
+
+For detailed return-object and model documentation, follow the linked Wiki pages. For the stable 1.x public API contract and current async endpoint coverage, see [public-api.md](public-api.md).
+
+**Jump to:** [People](#people-person-players-coaches) · [Teams](#teams) · [Stats](#stats) · [Games](#games) · [Schedules](#schedules) · [Venues](#venues) · [Sports](#sports) · [Leagues](#leagues) · [Divisions](#divisions) · [Seasons](#seasons) · [Standings](#standings) · [Draft](#draft) · [Awards](#awards) · [Gamepace](#gamepace)
+
+## People, Person, Players, Coaches
+
+[Wiki: People](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-People)
+
+| Method | Description |
+| --- | --- |
+| `get_people_id()` | Return person ID(s) from a full name |
+| `get_person()` | Return a person from an ID |
+| `get_people()` | Return all players for a sport |
+
+```text
+Mlb.get_people_id(fullname: str, sport_id: int = 1, search_key: str = 'fullname', **params)
+Mlb.get_person(player_id: int, **params)
+Mlb.get_people(sport_id: int = 1, **params)
+```
+
+## Teams
+
+[Wiki: Team](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Team)
+
+| Method | Description |
+| --- | --- |
+| `get_team_id()` | Return team ID(s) from a name |
+| `get_team()` | Return a team from a team ID |
+| `get_teams()` | Return all teams for a sport |
+| `get_team_coaches()` | Return the coaching roster for a team |
+| `get_team_roster()` | Return the player roster for a team |
+
+```text
+Mlb.get_team_id(team_name: str, search_key: str = 'name', **params)
+Mlb.get_team(team_id: int, **params)
+Mlb.get_teams(sport_id: int = 1, **params)
+Mlb.get_team_coaches(team_id: int, **params)
+Mlb.get_team_roster(team_id: int, **params)
+```
+
+## Stats
+
+[Wiki: Stats](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Stats) · [Stats Guide](stats.md)
+
+| Method | Description |
+| --- | --- |
+| `get_player_stats()` | Return stats for a player |
+| `get_team_stats()` | Return stats for a team |
+| `get_stats()` | Return stats by stat type and group |
+| `get_players_stats_for_game()` | Return player stats for a game |
+
+```text
+Mlb.get_player_stats(person_id: int, stats: list, groups: list, **params)
+Mlb.get_team_stats(team_id: int, stats: list, groups: list, **params)
+Mlb.get_stats(stats: list, groups: list, **params: dict)
+Mlb.get_players_stats_for_game(person_id: int, game_id: int, **params)
+```
+
+The [Stats Guide](stats.md) includes runnable `Mlb` and `AsyncMlb` examples and explains the nested `stats[group][type]` return structure.
+
+## Games
+
+[Wiki: Game](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Game)
+
+| Method | Description |
+| --- | --- |
+| `get_game()` | Return a game for a game ID |
+| `get_game_play_by_play()` | Return play-by-play data for a game |
+| `get_game_line_score()` | Return a linescore for a game |
+| `get_game_box_score()` | Return a boxscore for a game |
+
+```text
+Mlb.get_game(game_id: int, **params)
+Mlb.get_game_play_by_play(game_id: int, **params)
+Mlb.get_game_line_score(game_id: int, **params)
+Mlb.get_game_box_score(game_id: int, **params)
+```
+
+## Schedules
+
+[Wiki: Schedule](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Schedule)
+
+| Method | Description |
+| --- | --- |
+| `get_schedule()` | Return a schedule from a date or date range |
+| `get_scheduled_games_by_date()` | Return scheduled games from dates |
+
+```text
+Mlb.get_schedule(date: str, start_date: str, end_date: str, sport_id: int, team_id: int, **params)
+Mlb.get_schedule(date: str = None, start_date: str = None, end_date: str = None, sport_id: int = 1, team_id: int = None, **params)
+Mlb.get_scheduled_games_by_date(date: str = None, start_date: str = None, end_date: str = None, sport_id: int = 1, **params)
+```
+
+## Venues
+
+[Wiki: Venue](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Venue)
+
+| Method | Description |
+| --- | --- |
+| `get_venue_id()` | Return venue ID(s) from a name |
+| `get_venue()` | Return a venue from an ID |
+| `get_venues()` | Return all venues |
+
+```text
+Mlb.get_venue_id(venue_name: str, search_key: str = 'name', **params)
+Mlb.get_venue(venue_id: int, **params)
+Mlb.get_venues(**params)
+```
+
+## Sports
+
+[Wiki: Sport](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Sport)
+
+| Method | Description |
+| --- | --- |
+| `get_sport()` | Return a sport from an ID |
+| `get_sports()` | Return all sports |
+| `get_sport_id()` | Return sport ID(s) from a name |
+
+```text
+Mlb.get_sport(sport_id: int, **params)
+Mlb.get_sports(**params)
+Mlb.get_sport_id(sport_name: str, search_key: str = 'name', **params)
+```
+
+## Leagues
+
+[Wiki: League](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-League)
+
+| Method | Description |
+| --- | --- |
+| `get_league()` | Return a league from an ID |
+| `get_leagues()` | Return all leagues |
+| `get_league_id()` | Return league ID(s) from a name |
+
+```text
+Mlb.get_league(league_id: int, **params)
+Mlb.get_leagues(**params)
+Mlb.get_league_id(league_name: str, search_key: str = 'name', **params)
+```
+
+## Divisions
+
+[Wiki: Division](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Division)
+
+| Method | Description |
+| --- | --- |
+| `get_division()` | Return a division from an ID |
+| `get_divisions()` | Return all divisions |
+| `get_division_id()` | Return division ID(s) from a name |
+
+```text
+Mlb.get_division(division_id: int, **params)
+Mlb.get_divisions(**params)
+Mlb.get_division_id(division_name: str, search_key: str = 'name', **params)
+```
+
+## Seasons
+
+[Wiki: Season](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Season)
+
+| Method | Description |
+| --- | --- |
+| `get_season()` | Return a season |
+| `get_seasons()` | Return all seasons |
+
+```text
+Mlb.get_season(season_id: str, sport_id: int = None, **params)
+Mlb.get_seasons(sportid: int = None, **params)
+```
+
+## Standings
+
+[Wiki: Standings](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Standings)
+
+| Method | Description |
+| --- | --- |
+| `get_standings()` | Return standings for a league and season |
+
+```text
+Mlb.get_standings(league_id: int, season: str, **params)
+```
+
+## Draft
+
+[Wiki: Draft](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Draft(round))
+
+| Method | Description |
+| --- | --- |
+| `get_draft()` | Return a draft for a given year |
+
+```text
+Mlb.get_draft(year_id: int, **params)
+```
+
+## Awards
+
+[Wiki: Award](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Award)
+
+| Method | Description |
+| --- | --- |
+| `get_awards()` | Return award recipients for an award |
+
+```text
+Mlb.get_awards(award_id: int, **params)
+```
+
+## Gamepace
+
+[Wiki: Gamepace](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Gamepace)
+
+| Method | Description |
+| --- | --- |
+| `get_gamepace()` | Return pace-of-game metrics for a sport, league, or team |
+
+```text
+Mlb.get_gamepace(season: str, sport_id=1, **params)
+```
diff --git a/docs/stats.md b/docs/stats.md
new file mode 100644
index 0000000..0a30184
--- /dev/null
+++ b/docs/stats.md
@@ -0,0 +1,272 @@
+# Stats Guide
+
+The stats methods return MLB statistics grouped by **stat group** and then by **stat type**. Both `Mlb` and `AsyncMlb` return the same structure.
+
+## Stats methods
+
+| Method | Use |
+| --- | --- |
+| `get_player_stats()` | Stats for one player |
+| `get_team_stats()` | Stats for one team |
+| `get_stats()` | General stats query across the Stats API |
+| `get_players_stats_for_game()` | Stats for one player in one game |
+
+The synchronous and asynchronous signatures match. With `AsyncMlb`, await the method call.
+
+## Understanding the return value
+
+The four stats methods return a nested dictionary:
+
+```text
+stats[group][type] -> Stat
+```
+
+For example:
+
+```python
+stats = mlb.get_player_stats(
+ 664034,
+ stats=["season"],
+ groups=["hitting"],
+ season=2022,
+)
+
+season_hitting = stats["hitting"]["season"]
+```
+
+`season_hitting` is a `Stat` model. Its `splits` field contains the returned stat splits.
+
+```python
+for split in season_hitting.splits:
+ print(split.stat.model_dump(exclude_none=True))
+```
+
+A query can request multiple groups and stat types at once:
+
+```python
+stats = mlb.get_player_stats(
+ 664034,
+ stats=["season", "career"],
+ groups=["hitting", "fielding"],
+ season=2022,
+)
+
+for group_name, group_stats in stats.items():
+ for stat_type, stat in group_stats.items():
+ print(group_name, stat_type, stat.total_splits)
+```
+
+If the API response contains no usable stats, these methods return `{}`.
+
+## Player stats
+
+Use `get_player_stats()` when you know the MLB person ID and want one or more stat types for that player.
+
+### Sync
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ stats = mlb.get_player_stats(
+ 664034,
+ stats=["season", "career"],
+ groups=["hitting"],
+ season=2022,
+ )
+
+season = stats["hitting"]["season"]
+for split in season.splits:
+ print(split.stat.model_dump(exclude_none=True))
+```
+
+### Async
+
+```python
+import asyncio
+
+from mlbstatsapi import AsyncMlb
+
+
+async def main():
+ async with AsyncMlb() as mlb:
+ stats = await mlb.get_player_stats(
+ 664034,
+ stats=["season", "career"],
+ groups=["hitting"],
+ season=2022,
+ )
+
+ season = stats["hitting"]["season"]
+ for split in season.splits:
+ print(split.stat.model_dump(exclude_none=True))
+
+
+asyncio.run(main())
+```
+
+## Team stats
+
+Use `get_team_stats()` for stat data scoped to one team.
+
+### Sync
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ stats = mlb.get_team_stats(
+ 136,
+ stats=["season", "seasonAdvanced"],
+ groups=["hitting"],
+ season=2022,
+ )
+
+for stat_type, stat in stats["hitting"].items():
+ print(stat_type)
+ for split in stat.splits:
+ print(split.stat.model_dump(exclude_none=True))
+```
+
+### Async
+
+```python
+import asyncio
+
+from mlbstatsapi import AsyncMlb
+
+
+async def main():
+ async with AsyncMlb() as mlb:
+ stats = await mlb.get_team_stats(
+ 136,
+ stats=["season", "seasonAdvanced"],
+ groups=["hitting"],
+ season=2022,
+ )
+
+ for stat_type, stat in stats["hitting"].items():
+ print(stat_type)
+ for split in stat.splits:
+ print(split.stat.model_dump(exclude_none=True))
+
+
+asyncio.run(main())
+```
+
+## General stats queries
+
+`get_stats()` queries the general `/stats` endpoint. Additional keyword arguments can narrow the request by season, team, league, game type, sport, and other Stats API parameters.
+
+### Sync
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ stats = mlb.get_stats(
+ stats=["season"],
+ groups=["hitting"],
+ season=2022,
+ sportIds=1,
+ )
+
+for group_name, group_stats in stats.items():
+ for stat_type, stat in group_stats.items():
+ print(group_name, stat_type)
+ for split in stat.splits:
+ print(split.stat.model_dump(exclude_none=True))
+```
+
+### Async
+
+```python
+import asyncio
+
+from mlbstatsapi import AsyncMlb
+
+
+async def main():
+ async with AsyncMlb() as mlb:
+ stats = await mlb.get_stats(
+ stats=["season"],
+ groups=["hitting"],
+ season=2022,
+ sportIds=1,
+ )
+
+ for group_name, group_stats in stats.items():
+ for stat_type, stat in group_stats.items():
+ print(group_name, stat_type)
+ for split in stat.splits:
+ print(split.stat.model_dump(exclude_none=True))
+
+
+asyncio.run(main())
+```
+
+## Player stats for a game
+
+Use `get_players_stats_for_game()` when you have both the player's MLB person ID and the game's `gamePk`.
+
+### Sync
+
+```python
+from mlbstatsapi import Mlb
+
+with Mlb() as mlb:
+ stats = mlb.get_players_stats_for_game(
+ person_id=663728,
+ game_id=715757,
+ )
+
+for group_name, group_stats in stats.items():
+ for stat_type, stat in group_stats.items():
+ print(group_name, stat_type)
+ for split in stat.splits:
+ print(split.stat.model_dump(exclude_none=True))
+```
+
+### Async
+
+```python
+import asyncio
+
+from mlbstatsapi import AsyncMlb
+
+
+async def main():
+ async with AsyncMlb() as mlb:
+ stats = await mlb.get_players_stats_for_game(
+ person_id=663728,
+ game_id=715757,
+ )
+
+ for group_name, group_stats in stats.items():
+ for stat_type, stat in group_stats.items():
+ print(group_name, stat_type)
+ for split in stat.splits:
+ print(split.stat.model_dump(exclude_none=True))
+
+
+asyncio.run(main())
+```
+
+## Finding valid stat types and groups
+
+The MLB Stats API publishes the available values directly:
+
+- Stat types:
+- Stat groups:
+- Event types:
+- Game types:
+
+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`.
+
+## Related documentation
+
+- [Method reference](methods.md)
+- [General usage examples](examples.md)
+- [Async usage](async.md)
+- [Public API contract](public-api.md)
+- [Stats model Wiki](https://github.com/zero-sum-seattle/python-mlb-statsapi/wiki/Data-Types:-Stats)