> ## Documentation Index
> Fetch the complete documentation index at: https://tif1.tracinginsights.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Core API

> Complete reference for Session, Driver, Lap, Laps, and Telemetry classes - the foundation of tif1

<Info>
  The core module is the heart of tif1, providing the primary interface for loading and working with Formula 1 data. All data access flows through the `Session` object, which serves as the central hub for accessing laps, telemetry, weather, race control messages, and driver information. The API is designed for maximum performance with async support, intelligent caching, and lazy loading patterns.
</Info>

## Overview

The core API is built around a hierarchical data model that mirrors the structure of Formula 1 sessions:

```
Session (2024 Monaco Grand Prix - Race)
├── Laps (all drivers, all laps)
├── Drivers (VER, HAM, LEC, ...)
│   └── Driver (VER)
│       ├── Laps (VER's laps only)
│       └── Lap (VER lap 19)
│           └── Telemetry (high-frequency data)
├── Weather (temperature, humidity, wind)
├── RaceControlMessages (flags, safety car)
├── Results (final classification)
└── CircuitInfo (corners, track layout)
```

**Key Design Principles:**

* **Lazy Loading**: Data is fetched only when accessed, minimizing unnecessary network requests. Properties like `session.laps`, `session.weather`, and `lap.telemetry` trigger data loading on first access.

* **Intelligent Caching**: Multi-layer caching strategy with in-memory LRU cache (instant access) and SQLite persistent cache (survives restarts). Cache keys are session-specific and backend-aware (pandas vs polars).

* **Async Support**: All data-intensive operations have async variants (`laps_async()`, `get_fastest_laps_tels_async()`) that use parallel HTTP requests for 4-5x faster loading compared to sequential access.

* **Backend Flexibility**: Choose between pandas (default, maximum compatibility) or polars (faster for large datasets, 10-100x speedup for some operations) for DataFrame operations. Backend selection is per-session and affects all data structures.

* **FastF1 Compatibility**: Drop-in replacement for FastF1 with the same API surface. All FastF1 methods (`pick_driver()`, `pick_fastest()`, `iterlaps()`, etc.) are supported with identical behavior.

* **Ultra-Cold Mode**: Special optimization mode that bypasses cache validation and skips loading unnecessary data for minimal latency. Ideal for serverless environments or when you only need specific data (e.g., fastest lap telemetry).

* **Error Resilience**: Comprehensive exception hierarchy (`DataNotFoundError`, `NetworkError`, `InvalidDataError`) with structured error context. Failed telemetry fetches are tracked per-driver to avoid repeated failures.

**Performance Characteristics:**

| Operation                 | Cold Cache | Warm Cache | Notes                                   |
| ------------------------- | ---------- | ---------- | --------------------------------------- |
| `session.laps`            | 2-4s       | \<100ms    | Auto-async parallel loading             |
| `laps_async()`            | 0.5-1s     | \<100ms    | Explicit async, 4-5x faster             |
| `get_fastest_lap_tel()`   | 200-500ms  | \<50ms     | Single telemetry fetch                  |
| `get_fastest_laps_tels()` | 300-800ms  | \<100ms    | Parallel telemetry for multiple drivers |
| `ultra_cold=True`         | 100-300ms  | N/A        | Skips cache validation                  |

**Memory Footprint:**

* Session metadata: \~10-50 KB
* Laps DataFrame (20 drivers, 60 laps): \~500 KB - 2 MB
* Single lap telemetry: \~50-200 KB (varies by track length)
* Full session telemetry (all laps): \~50-200 MB (rarely needed)

**Thread Safety:**

* Session objects are thread-safe for read operations
* Internal caches use threading locks for concurrent access
* Async methods can be called from multiple coroutines safely

## `get_session`

The main entry point for loading F1 session data. This function handles event name resolution, session validation, and returns a configured `Session` object ready to load data.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_session(
    year: int,
    gp: str | int,
    session: str | int,
    enable_cache: bool | None = None,
    lib: Literal["pandas", "polars"] | None = None
) -> Session
```

**Parameters:**

<ParamField path="year" type="int" required>
  Season year (2018-current). Must be within the supported range.

  **Valid range**: 2018 to current season

  **Example**: `2024`, `2025`
</ParamField>

<ParamField path="gp" type="str | int" required>
  Grand Prix identifier. Can be specified in multiple ways:

  * **Full name**: `"Monaco Grand Prix"`, `"British Grand Prix"`
  * **Abbreviated name**: `"Monaco"`, `"Silverstone"` (uses fuzzy matching)
  * **Round number**: `6`, `12` (1-indexed, based on calendar order)

  The function automatically resolves abbreviated names and round numbers to the official event name.

  **Example**: `"Monaco Grand Prix"`, `"Monaco"`, `6`
</ParamField>

<ParamField path="session" type="str | int" required>
  Session identifier. Can be specified as:

  * **Full name**: `"Practice 1"`, `"Qualifying"`, `"Race"`
  * **Abbreviated**: `"FP1"`, `"FP2"`, `"FP3"`, `"Q"`, `"S"` (Sprint), `"R"`
  * **Session number**: `1` (Practice 1), `2` (Practice 2), `3` (Practice 3), `4` (Qualifying), `5` (Race)

  Session numbers are 1-indexed and follow the weekend schedule order.

  **Example**: `"Qualifying"`, `"Q"`, `4`
</ParamField>

<ParamField path="enable_cache" type="bool | None" default="None">
  Enable or disable caching for this session.

  * `True`: Enable caching (recommended for production)
  * `False`: Disable caching (useful for testing or forcing fresh data)
  * `None`: Use global config value (default behavior)

  **Caching behavior**: When enabled, all fetched data (laps, telemetry, weather, messages) is stored in a local SQLite database. Subsequent access to the same data reads from cache instead of making network requests.

  **Cache location**: `~/.tif1/cache/tif1_cache.db`
</ParamField>

<ParamField path="lib" type="Literal['pandas', 'polars'] | None" default="None">
  DataFrame library choice for data representation.

  * `"pandas"`: Use pandas DataFrames (default, maximum compatibility)
  * `"polars"`: Use polars DataFrames (faster for large datasets, requires `polars` package)
  * `None`: Use global config value (defaults to `"pandas"`)

  **Performance note**: Polars can be significantly faster for large datasets (10-100x for some operations), but requires the `polars` package to be installed. If polars is requested but not available, tif1 automatically falls back to pandas with a warning.
</ParamField>

**Returns:**

<ResponseField name="Session" type="Session">
  A configured `Session` object ready to load data. The session is not loaded until you access its properties (lazy loading) or explicitly call `session.load()`.
</ResponseField>

**Raises:**

<ResponseField name="ValueError" type="Exception">
  Raised in the following cases:

  * Year is outside the supported range (\< 2018 or > current season)
  * Grand Prix name/round cannot be resolved
  * Session name/number doesn't exist for the specified event
  * Session number is out of range for the event
</ResponseField>

<ResponseField name="DataNotFoundError" type="Exception">
  Raised when the specified year, GP, or session doesn't exist in the data source.
</ResponseField>

<ResponseField name="NetworkError" type="Exception">
  Raised when all CDN sources fail to respond or return errors.
</ResponseField>

**Examples:**

<CodeGroup>
  ```python Basic Usage theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import tif1

  # By full name
  session = tif1.get_session(2024, "Monaco Grand Prix", "Race")

  # By abbreviated name (fuzzy matching)
  session = tif1.get_session(2024, "Monaco", "Race")

  # By round number
  session = tif1.get_session(2024, 6, "Race")  # Round 6

  # By session abbreviation
  session = tif1.get_session(2024, "Monaco", "Q")  # Qualifying

  # By session number
  session = tif1.get_session(2024, "Monaco", 5)  # Session 5 = Race
  ```

  ```python With Options theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import tif1

  # Disable caching for fresh data
  session = tif1.get_session(
      2024,
      "Monaco Grand Prix",
      "Race",
      enable_cache=False
  )

  # Use polars backend for better performance
  session = tif1.get_session(
      2024,
      "Monaco Grand Prix",
      "Race",
      lib="polars"
  )

  # Combine options
  session = tif1.get_session(
      2024,
      "Monaco",
      "Q",
      enable_cache=True,
      lib="pandas"
  )
  ```

  ```python Error Handling theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import tif1
  from tif1.exceptions import DataNotFoundError, NetworkError

  try:
      session = tif1.get_session(2024, "Monaco", "Race")
      laps = session.laps
  except ValueError as e:
      print(f"Invalid input: {e}")
  except DataNotFoundError as e:
      print(f"Data not found: {e}")
  except NetworkError as e:
      print(f"Network error: {e}")
  ```
</CodeGroup>

<Tip>
  **Best Practice**: Use abbreviated names and session codes for cleaner code:

  ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  # ✅ Concise and readable
  session = tif1.get_session(2024, "Monaco", "Q")

  # ❌ Verbose
  session = tif1.get_session(2024, "Monaco Grand Prix", "Qualifying")
  ```
</Tip>

<Note>
  **Event Name Resolution**: The function uses fuzzy matching to resolve abbreviated event names. For example, `"Silverstone"` resolves to `"British Grand Prix"`, and `"Spa"` resolves to `"Belgian Grand Prix"`. If fuzzy matching fails, the original string is used as-is.
</Note>

***

## Session

The `Session` object is the central hub for all data related to a specific F1 weekend session. It provides access to lap timing data, telemetry, weather conditions, race control messages, driver information, and session results.

**Design Philosophy:**

* **Lazy Loading**: Properties are loaded on first access, not at construction time
* **Caching**: All fetched data is cached (when enabled) to minimize network requests
* **Immutable Identity**: Once created, a session's year/GP/session cannot be changed
* **Thread-Safe**: Internal caches use locks for concurrent access
* **Memory Efficient**: Only requested data is loaded into memory

### Constructor

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
Session(
    year: int,
    gp: str,
    session: str,
    enable_cache: bool | None = None,
    lib: Literal["pandas", "polars"] | None = None
)
```

<Warning>
  **Direct Construction Not Recommended**: Use `get_session()` instead of constructing `Session` directly. The `get_session()` function handles name normalization, validation, and ensures the session exists before creating the object.

  ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  # ✅ Recommended
  session = tif1.get_session(2024, "Monaco", "Race")

  # ❌ Not recommended (no validation)
  session = Session(2024, "Monaco_Grand_Prix", "Race")
  ```
</Warning>

**Constructor Parameters:**

<ParamField path="year" type="int" required>
  Season year (2018-current). Must be within the supported range.
</ParamField>

<ParamField path="gp" type="str" required>
  URL-encoded Grand Prix name (e.g., `"Monaco_Grand_Prix"`). Spaces should be replaced with underscores or `%20`.

  **Note**: `get_session()` handles this encoding automatically.
</ParamField>

<ParamField path="session" type="str" required>
  URL-encoded session name (e.g., `"Qualifying"`, `"Race"`).
</ParamField>

<ParamField path="enable_cache" type="bool | None" default="None">
  Enable/disable caching. If `None`, uses global config value.
</ParamField>

<ParamField path="lib" type="Literal['pandas', 'polars'] | None" default="None">
  DataFrame library choice. If `None`, uses global config value (defaults to `"pandas"`).
</ParamField>

### Properties

<ResponseField name="year" type="int">
  The season year (e.g., 2025).
</ResponseField>

<ResponseField name="gp" type="str">
  URL-encoded Grand Prix name (e.g., "Monaco\_Grand\_Prix").
</ResponseField>

<ResponseField name="session" type="str">
  URL-encoded session name (e.g., "Qualifying", "Race").
</ResponseField>

<ResponseField name="lib" type="Literal['pandas', 'polars']">
  The DataFrame lib being used for this session.
</ResponseField>

<ResponseField name="enable_cache" type="bool">
  Whether caching is enabled for this session.
</ResponseField>

<ResponseField name="drivers" type="list[str]">
  List of driver numbers as strings in the session (e.g., \["1", "33", "44"]).
  Loaded lazily on first access.

  <Note>
    This returns driver NUMBERS, not driver codes. For driver codes (e.g., "VER", "HAM"), use `drivers_df["Driver"]`.
  </Note>
</ResponseField>

<ResponseField name="driver_list" type="list[str]">
  Alias for `drivers` property. Returns driver numbers as strings.
</ResponseField>

<ResponseField name="drivers_df" type="DataFrame">
  DataFrame with driver information including:

  * `Abbreviation`: 3-letter driver code (e.g., "VER", "HAM")
  * `TeamName`: Team name
  * `DriverNumber`: Car number as string
  * `FirstName`: Driver's first name
  * `LastName`: Driver's last name
  * `FullName`: Full name (FirstName + LastName)
  * `TeamColor`: Hex color code
  * `HeadshotUrl`: URL to driver photo

  Loaded lazily on first access.
</ResponseField>

<ResponseField name="laps" type="DataFrame">
  All laps for all drivers. Includes weather data automatically merged.
  Loaded lazily on first access. Use `laps_async()` for faster parallel loading.
</ResponseField>

<ResponseField name="weather" type="DataFrame">
  Weather data recorded during the session with columns:

  * `Time`: Timestamp
  * `AirTemp`: Air temperature (°C)
  * `TrackTemp`: Track temperature (°C)
  * `Humidity`: Relative humidity (%)
  * `Pressure`: Atmospheric pressure (mbar)
  * `WindSpeed`: Wind speed (km/h)
  * `WindDirection`: Wind direction (degrees)
  * `Rainfall`: Rainfall indicator
</ResponseField>

<ResponseField name="weather_data" type="DataFrame">
  Alias for `weather` property.
</ResponseField>

<ResponseField name="race_control_messages" type="DataFrame">
  Official messages from Race Control with columns:

  * `Time`: Message timestamp
  * `Category`: Message category (e.g., "Flag", "SafetyCar")
  * `Message`: Message text
  * `Status`: Track status code
  * `Flag`: Flag type
  * `Scope`: Message scope
  * `Sector`: Affected sector
  * `RacingNumber`: Affected driver number
</ResponseField>

<ResponseField name="results" type="SessionResults">
  Final classification/results for the session. Contains:

  * `Position`: Final position
  * `Driver`: Driver code
  * `Team`: Team name
  * `Points`: Championship points earned
  * `Status`: Finish status
  * `Time`: Total time or gap
</ResponseField>

<ResponseField name="car_data" type="DataFrame">
  Car telemetry data aggregated across all laps with columns:

  * `Time`: Timestamp
  * `Speed`: Speed (km/h)
  * `RPM`: Engine RPM
  * `nGear`: Gear number
  * `Throttle`: Throttle position (%)
  * `Brake`: Brake status
  * `DRS`: DRS status
</ResponseField>

<ResponseField name="pos_data" type="DataFrame">
  Position data for all cars with columns:

  * `Time`: Timestamp
  * `X`, `Y`, `Z`: 3D coordinates (meters)
  * `Status`: Car status
  * `Driver`: Driver code
</ResponseField>

<ResponseField name="session_info" type="dict">
  Session metadata including start time, circuit info, and session status.
</ResponseField>

<ResponseField name="name" type="str">
  Human-readable session name (e.g., "Monaco Grand Prix - Qualifying").
</ResponseField>

<ResponseField name="date" type="datetime">
  Session date and time.
</ResponseField>

<ResponseField name="event" type="dict">
  Event information including circuit details and schedule.
</ResponseField>

<ResponseField name="session_start_time" type="datetime">
  Official session start time.
</ResponseField>

<ResponseField name="t0_date" type="datetime">
  Reference time (t=0) for the session.
</ResponseField>

<ResponseField name="session_status" type="list">
  Session status changes throughout the session.
</ResponseField>

<ResponseField name="track_status" type="DataFrame">
  Track status changes (flags, safety car, etc.) with timestamps.
</ResponseField>

<ResponseField name="total_laps" type="int | None">
  Total number of laps completed in the session.

  <Note>
    This property is not yet implemented and currently returns `None`.
  </Note>
</ResponseField>

### Methods

#### `load()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def load(
    laps: bool = True,
    telemetry: bool = True,
    weather: bool = True,
    messages: bool = True
) -> Session
```

Explicitly load session data. By default, data is loaded lazily when accessed.

**Parameters:**

* `laps`: Load lap timing data
* `telemetry`: Prefetch telemetry for all laps (expensive)
* `weather`: Load weather data
* `messages`: Load race control messages

**Returns:**

* Self (for method chaining)

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Load only laps and weather
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load(laps=True, telemetry=False, weather=True, messages=False)

# Or use lazy loading (recommended)
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
laps = session.laps  # Loads automatically
```

#### `laps_async()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def laps_async() -> DataFrame
```

Asynchronously load all laps for all drivers using parallel HTTP requests. This is the fastest way to initialize a session.

**Returns:**

* DataFrame with all laps

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import tif1

async def main():
    session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
    laps = await session.laps_async()
    print(f"Loaded {len(laps)} laps")

asyncio.run(main())
```

#### `get_driver()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_driver(driver: str | int) -> Driver
```

Get a `Driver` object for the specified driver.

**Parameters:**

* `driver`: 3-letter driver code (e.g., "VER") or driver number (e.g., 33)

**Returns:**

* `Driver` object

**Raises:**

* `DriverNotFoundError`: If the driver doesn't exist in this session

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
ver = session.get_driver("VER")
ver = session.get_driver(33)  # By number
```

#### `get_fastest_laps()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_fastest_laps(
    by_driver: bool = True,
    drivers: list[str] | None = None
) -> DataFrame
```

Get the fastest lap(s) from the session.

**Parameters:**

* `by_driver`: If `True`, returns fastest lap per driver. If `False`, returns single overall fastest lap
* `drivers`: Optional list of driver codes to filter. If `None`, includes all drivers

**Returns:**

* DataFrame with fastest lap(s)

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Fastest lap per driver
fastest = session.get_fastest_laps(by_driver=True)

# Overall fastest lap
fastest = session.get_fastest_laps(by_driver=False)

# Fastest laps for specific drivers
fastest = session.get_fastest_laps(by_driver=True, drivers=["VER", "HAM", "BOT"])
```

#### `get_fastest_laps_async()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def get_fastest_laps_async(
    by_driver: bool = True,
    drivers: list[str] | None = None
) -> DataFrame
```

Async version of `get_fastest_laps()` for parallel data fetching.

#### `get_fastest_lap_tel()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_fastest_lap_tel(ultra_cold: bool | None = None) -> DataFrame
```

Optimized method to get telemetry for the single overall fastest lap of the session. Uses ultra-cold start optimization to minimize latency.

**Parameters:**

* `ultra_cold`: Enable ultra-low latency mode. If `None`, uses global config

**Returns:**

* DataFrame with telemetry data

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Get fastest lap telemetry
tel = session.get_fastest_lap_tel()
print(f"Max speed: {tel['Speed'].max()} km/h")
```

#### `get_fastest_lap_tel_async()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def get_fastest_lap_tel_async(
    ultra_cold: bool | None = None
) -> DataFrame
```

Async version of `get_fastest_lap_tel()`.

#### `get_fastest_laps_tels()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_fastest_laps_tels(
    by_driver: bool = True,
    drivers: list[str] | None = None,
    ultra_cold: bool | None = None
) -> dict[str, DataFrame]
```

Parallel fetch telemetry for multiple drivers' fastest laps. Significantly faster than fetching telemetry sequentially.

**Parameters:**

* `by_driver`: If `True`, gets fastest lap per driver. If `False`, gets overall fastest
* `drivers`: Optional list of driver codes to filter
* `ultra_cold`: Enable ultra-low latency mode

**Returns:**

* Dictionary mapping driver codes to telemetry DataFrames

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Get telemetry for all drivers' fastest laps
tels = session.get_fastest_laps_tels(by_driver=True)

for driver, tel in tels.items():
    print(f"{driver}: {tel['Speed'].max():.1f} km/h")

# Get telemetry for specific drivers
tels = session.get_fastest_laps_tels(
    by_driver=True,
    drivers=["VER", "HAM", "BOT"]
)
```

<Tip>
  **Performance Tip:** This method fetches telemetry for multiple drivers in parallel, which is
  significantly faster than calling `get_fastest_lap_tel()` sequentially for each driver.
</Tip>

#### `get_fastest_laps_tels_async()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def get_fastest_laps_tels_async(
    by_driver: bool = True,
    drivers: list[str] | None = None,
    ultra_cold: bool | None = None
) -> dict[str, DataFrame]
```

Async version of `get_fastest_laps_tels()`.

#### `fetch_driver_laps_parallel()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def fetch_driver_laps_parallel(
    drivers: list[str]
) -> dict[str, DataFrame]
```

Fetch lap data for multiple drivers in parallel.

**Parameters:**

* `drivers`: List of driver codes

**Returns:**

* Dictionary mapping driver codes to lap DataFrames

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio

async def main():
    session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
    laps_dict = await session.fetch_driver_laps_parallel(["VER", "HAM", "BOT"])

    for driver, laps in laps_dict.items():
        print(f"{driver}: {len(laps)} laps")

asyncio.run(main())
```

#### `fetch_all_laps_telemetry()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def fetch_all_laps_telemetry(
    drivers: list[str] | None = None,
    ultra_cold: bool | None = None
) -> dict[tuple[str, int], DataFrame]
```

Fetch telemetry for all laps of specified drivers synchronously.

**Parameters:**

* `drivers`: Optional list of driver codes. If `None`, fetches for all drivers
* `ultra_cold`: Enable ultra-low latency mode

**Returns:**

* Dictionary mapping (driver, lap\_number) tuples to telemetry DataFrames

**Warning:**
This can be very slow and data-intensive (50-200 MB for full session). Use sparingly. Prefer the async version for better performance.

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Fetch all telemetry for specific drivers (slow!)
all_tels = session.fetch_all_laps_telemetry(drivers=["VER", "HAM"])

for (driver, lap_num), tel in all_tels.items():
    print(f"{driver} Lap {lap_num}: {len(tel)} samples")
```

#### `fetch_all_laps_telemetry_async()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
async def fetch_all_laps_telemetry_async(
    drivers: list[str] | None = None,
    ultra_cold: bool | None = None
) -> dict[tuple[str, int], DataFrame]
```

Async version of `fetch_all_laps_telemetry()`. Fetches telemetry for all laps in parallel, significantly faster than the sync version.

**Performance:** 5-10x faster than sync version due to parallel HTTP requests.

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import tif1

async def main():
    session = tif1.get_session(2024, "Monaco", "Race")
    # Fetch telemetry for all laps of specific drivers
    all_tels = await session.fetch_all_laps_telemetry_async(
        drivers=["VER", "HAM"]
    )

    for (driver, lap_num), tel in all_tels.items():
        print(f"{driver} Lap {lap_num}: {len(tel)} samples, "
              f"max speed: {tel['Speed'].max():.1f} km/h")

asyncio.run(main())
```

<Warning>
  **Memory Warning:** Fetching all telemetry for a full race session can consume 50-200 MB of memory. Only use this when you genuinely need all telemetry data. For most use cases, fetch telemetry for specific laps or drivers instead.
</Warning>

#### `get_circuit_info()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_circuit_info() -> CircuitInfo
```

Get circuit information including track length and corner locations.

**Returns:**

* `CircuitInfo` object with:
  * `corners`: DataFrame with columns `X, Y, Number, Letter, Angle, Distance`
  * `marshal_lights`: Empty DataFrame (not available in data source)
  * `marshal_sectors`: Empty DataFrame (not available in data source)
  * `rotation`: Circuit rotation in degrees (float)

**Caching:** Results are cached on the session object after the first call.

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
circuit = session.get_circuit_info()

# Access corner data
print(f"Number of corners: {len(circuit.corners)}")
print(circuit.corners[["Number", "X", "Y", "Distance"]].head())

# Add distance markers to corners using a reference lap
ver = session.get_driver("VER")
fastest_lap = ver.get_lap(19)
circuit.add_marker_distance(fastest_lap)

# Now corners have accurate distance values
print(circuit.corners[["Number", "Distance"]].head())
```

<Note>
  **FastF1 Compatibility:** This method returns a `CircuitInfo` dataclass that is fully compatible with FastF1's `mvapi.CircuitInfo`. The `add_marker_distance()` method works identically to FastF1's implementation.
</Note>

***

## Driver

Represents a specific driver's performance within a session. Provides convenient access to driver-specific lap data and telemetry. The `Driver` class extends `pd.Series`, so driver metadata is accessible as Series data.

**Design Philosophy:**

* **Lazy Loading:** Lap data is loaded on first access to `driver.laps`
* **Efficient Filtering:** When session laps are already loaded, driver laps are filtered in-place without additional network requests
* **Prefetching:** The `get_driver()` method can prefetch driver laps in parallel with driver metadata for faster initialization
* **Caching:** Lap index maps are cached for O(1) lap number lookups

**Performance Characteristics:**

* First access to `driver.laps`: 200-500ms (cold cache), \<50ms (warm cache)
* Subsequent access: \<1ms (in-memory reference)
* `get_lap(n)`: O(1) lookup using cached index map
* `get_fastest_lap()`: O(n) scan of lap times (typically \<1ms for 60 laps)

### Constructor

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
Driver(
    session: Session,
    driver: str,
    prefetched_lap_data: dict[str, Any] | None = None
)
```

<Note>
  Use `session.get_driver()` instead of constructing `Driver` directly. The `Driver` class extends `pd.Series` and contains driver metadata as Series data.
</Note>

### Properties

<ResponseField name="driver" type="str">
  The 3-letter driver code (e.g., "VER", "HAM", "LEC").
</ResponseField>

<ResponseField name="laps" type="DataFrame">
  All laps completed by this driver. Loaded lazily on first access.
  Contains all lap timing data including sector times, compounds, stint info, etc.
</ResponseField>

<ResponseField name="session" type="Session">
  Reference to the parent session object.
</ResponseField>

### Series Data

Since `Driver` extends `pd.Series`, driver metadata is accessed via dictionary-style indexing:

* `driver["DriverNumber"]`: Car number as string (e.g., "33", "44", "16")
* `driver["Abbreviation"]`: 3-letter driver code (same as `driver.driver`)
* `driver["TeamName"]`: Team name (e.g., "Red Bull Racing", "Mercedes")
* `driver["TeamColor"]`: Hex color code
* `driver["FirstName"]`: Driver's first name
* `driver["LastName"]`: Driver's last name
* `driver["FullName"]`: Full name (FirstName + LastName)
* `driver["HeadshotUrl"]`: URL to driver photo

### Methods

#### `get_lap()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_lap(lap_number: int) -> Lap
```

Get a specific lap by number.

**Parameters:**

* `lap_number`: The lap number to retrieve (1-indexed)

**Returns:**

* `Lap` object for the specified lap

**Raises:**

* `LapNotFoundError`: If the lap doesn't exist for this driver

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
ver = session.get_driver("VER")
lap_19 = ver.get_lap(19)
print(f"Lap time: {lap_19.lap_time:.3f}s")
```

#### `get_fastest_lap()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_fastest_lap() -> DataFrame
```

Get this driver's fastest lap as a single-row DataFrame.

**Returns:**

* Single-row DataFrame with the fastest lap data

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
fastest = ver.get_fastest_lap()
print(f"Fastest: {fastest['LapTime'].iloc[0]:.3f}s")
print(f"Compound: {fastest['Compound'].iloc[0]}")
```

#### `get_fastest_lap_tel()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_fastest_lap_tel() -> DataFrame
```

Get telemetry for this driver's fastest lap. Uses the session's ultra-cold mode setting.

**Returns:**

* DataFrame with telemetry data (Time, Speed, RPM, Throttle, etc.)

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
fastest_tel = ver.get_fastest_lap_tel()
print(f"Max speed: {fastest_tel['Speed'].max():.1f} km/h")
print(f"Avg throttle: {fastest_tel['Throttle'].mean():.1f}%")
```

***

## Laps

The `Laps` class extends `pd.DataFrame` and provides a rich set of methods for filtering and analyzing lap timing data. It represents a collection of laps (either for all drivers or a specific driver) with FastF1-compatible filtering methods.

**Design Philosophy:**

* **DataFrame Extension:** Inherits all pandas DataFrame methods while adding F1-specific functionality
* **Method Chaining:** Most methods return new `Laps` objects, enabling fluent API patterns
* **FastF1 Compatibility:** All `pick_*` methods match FastF1's behavior exactly
* **Lazy Telemetry:** The `telemetry` property loads telemetry only when accessed

### Filtering Methods

#### `pick_driver(identifier)` / `pick_drivers(identifiers)`

Filter laps by driver(s). Accepts driver codes, numbers, or driver objects.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Single driver
ver_laps = laps.pick_driver("VER")
ver_laps = laps.pick_driver(33)  # By number

# Multiple drivers
top_3_laps = laps.pick_drivers(["VER", "HAM", "LEC"])
```

#### `pick_lap(lap_number)` / `pick_laps(laps)`

Filter by lap number(s). Supports individual numbers, lists, or slices.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Single lap
lap_1 = laps.pick_lap(1)

# Multiple laps
first_10 = laps.pick_laps(range(1, 11))

# Slice notation
first_10 = laps.pick_laps(slice(1, 10))
middle_laps = laps.pick_laps(slice(10, 50))
from_lap_20 = laps.pick_laps(slice(20, None))  # Lap 20 onwards
```

#### `pick_team(name)` / `pick_teams(names)`

Filter laps by team name(s).

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Single team
rb_laps = laps.pick_team("Red Bull Racing")

# Multiple teams
top_teams = laps.pick_teams(["Red Bull Racing", "Mercedes", "Ferrari"])
```

#### `pick_fastest(only_by_time=False)`

Get the single fastest lap from the collection.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Get fastest lap
fastest = laps.pick_fastest()
print(f"Fastest: {fastest['Driver']} - {fastest['LapTimeSeconds']:.3f}s")

# Returns None if no valid laps
if fastest is not None:
    print(f"Compound: {fastest['Compound']}")
```

**Returns:** Single `Lap` object (pd.Series) or `None` if no valid laps.

#### `pick_quicklaps(threshold=1.07)`

Filter laps within a percentage of the fastest lap time. Default threshold is 107% (7% slower than fastest).

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Get all laps within 107% of fastest
quick_laps = laps.pick_quicklaps()

# Stricter threshold (within 103%)
very_quick = laps.pick_quicklaps(threshold=1.03)

# More lenient (within 110%)
decent_laps = laps.pick_quicklaps(threshold=1.10)
```

**Use Case:** Analyzing representative lap times, excluding outliers (slow laps, traffic, mistakes).

#### `pick_tyre(compound)` / `pick_compounds(compounds)`

Filter laps by tire compound.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Single compound
soft_laps = laps.pick_tyre("SOFT")

# Multiple compounds
slick_laps = laps.pick_compounds(["SOFT", "MEDIUM", "HARD"])
```

**Common Compounds:** `"SOFT"`, `"MEDIUM"`, `"HARD"`, `"INTERMEDIATE"`, `"WET"`

#### `pick_track_status(status, how="equals")`

Filter laps by track status code.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Green flag laps only
green_laps = laps.pick_track_status("1")

# Laps with any yellow flag
yellow_laps = laps.pick_track_status("4", how="contains")

# Safety car laps
sc_laps = laps.pick_track_status("6")
```

**Track Status Codes:**

* `"1"`: Green flag (normal racing)
* `"2"`: Yellow flag
* `"4"`: Safety Car
* `"5"`: Red flag
* `"6"`: Virtual Safety Car (VSC)
* `"7"`: VSC ending

**Parameters:**

* `how="equals"`: Exact match (default)
* `how="contains"`: Partial match (useful for compound status codes)

#### `pick_wo_box()` / `pick_box_laps(which="both")`

Filter laps by pit stop activity.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Laps without pit stops
clean_laps = laps.pick_wo_box()

# Laps with pit entry
pit_in_laps = laps.pick_box_laps(which="in")

# Laps with pit exit
pit_out_laps = laps.pick_box_laps(which="out")

# Any pit activity
pit_laps = laps.pick_box_laps(which="both")
```

#### `pick_not_deleted()` / `pick_accurate()`

Filter laps by data quality flags.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Exclude deleted laps (lap time deleted by FIA)
valid_laps = laps.pick_not_deleted()

# Only accurate laps (high-quality timing data)
accurate_laps = laps.pick_accurate()

# Combine filters for cleanest data
clean_laps = laps.pick_not_deleted().pick_accurate().pick_wo_box()
```

### Data Access Methods

#### `get_telemetry()` / `get_car_data()` / `get_pos_data()`

Get telemetry data for the laps. All three methods return the same data (FastF1 compatibility).

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Get telemetry for all laps
tel = laps.get_telemetry()

# Equivalent methods
tel = laps.get_car_data()
tel = laps.get_pos_data()

# Access via property
tel = laps.telemetry
```

**Returns:** `Telemetry` DataFrame with columns: `Time`, `Speed`, `RPM`, `Throttle`, `Brake`, `nGear`, `DRS`, `X`, `Y`, `Z`, `Distance`

**Note:** Only works for single-driver laps. Raises `ValueError` if laps contain multiple drivers.

#### `get_weather_data()`

Get weather data for the session.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
weather = laps.get_weather_data()
print(weather[["Time", "AirTemp", "TrackTemp", "Rainfall"]].head())
```

**Returns:** Weather DataFrame from the parent session.

#### `split_qualifying_sessions()`

Split qualifying laps into Q1, Q2, Q3 sessions.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
q1, q2, q3 = laps.split_qualifying_sessions()

print(f"Q1: {len(q1)} laps")
print(f"Q2: {len(q2)} laps")
print(f"Q3: {len(q3)} laps")

# Analyze each session separately
q3_fastest = q3.pick_fastest()
```

**Returns:** Tuple of three `Laps` objects (Q1, Q2, Q3). If qualifying session markers are unavailable, returns three copies of the full laps DataFrame.

#### `iterlaps(require=None)`

Iterate over laps with optional column requirements. Yields `(index, lap)` tuples where `lap` is a `Lap` object.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Iterate over all laps
for idx, lap in laps.iterlaps():
    print(f"Lap {lap['LapNumber']}: {lap['LapTimeSeconds']:.3f}s")

# Require specific columns
for idx, lap in laps.iterlaps(require=["LapTime", "Driver", "Compound"]):
    print(f"{lap['Driver']} on {lap['Compound']}: {lap['LapTime']}")

# Access telemetry in loop
for idx, lap in laps.iterlaps():
    tel = lap.telemetry
    if not tel.empty:
        print(f"Lap {lap['LapNumber']}: max speed {tel['Speed'].max():.1f} km/h")
```

**Parameters:**

* `require`: Optional list of column names that must be non-null. Laps with null values in these columns are skipped.

**Returns:** Generator yielding `_IterLapResult` tuples with:

* `.index`: DataFrame index of the lap
* `.lap`: `Lap` object (pd.Series) with session reference
* `[column_name]`: Direct column access (e.g., `result["Driver"]`)

**Performance:** Efficient iteration with minimal memory overhead. Telemetry is loaded lazily per lap.

### Utility Methods

#### `reset_index(drop=False, **kwargs)`

Reset DataFrame index. Automatically removes `level_0` column if created.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Reset index and drop old index
laps_reset = laps.reset_index(drop=True)

# Reset index and keep as column
laps_with_old_index = laps.reset_index(drop=False)
```

### Properties

#### `telemetry`

Get telemetry for all laps in the collection. Only works for single-driver laps.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
ver_laps = laps.pick_driver("VER")
tel = ver_laps.telemetry  # All telemetry for VER's laps

# Raises ValueError for multi-driver laps
# tel = laps.telemetry  # ERROR if laps contains multiple drivers
```

**Returns:** `Telemetry` DataFrame (concatenated telemetry from all laps).

**Raises:** `ValueError` if laps contain multiple drivers.

### Example: Complex Filtering

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import tif1

session = tif1.get_session(2024, "Monaco", "Race")
laps = session.laps

# Get clean racing laps for top 3 drivers on soft tires
clean_laps = (
    laps
    .pick_drivers(["VER", "HAM", "LEC"])
    .pick_tyre("SOFT")
    .pick_track_status("1")  # Green flag only
    .pick_not_deleted()
    .pick_accurate()
    .pick_wo_box()  # No pit stops
    .pick_quicklaps(threshold=1.05)  # Within 105% of fastest
)

print(f"Found {len(clean_laps)} clean laps")

# Analyze lap times by driver
for driver in ["VER", "HAM", "LEC"]:
    driver_laps = clean_laps.pick_driver(driver)
    if not driver_laps.empty:
        avg_time = driver_laps["LapTimeSeconds"].mean()
        print(f"{driver}: {avg_time:.3f}s average")
```

***

## Lap

Represents a single lap, providing access to high-frequency telemetry data. This is a lightweight wrapper around lap timing data with lazy telemetry loading. The `Lap` class extends `pd.Series`, so lap data is accessible as Series data.

### Constructor

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
Lap(
    session: Session,
    driver: str,
    lap_number: int
)
```

<Note>
  Use `driver.get_lap()` instead of constructing `Lap` directly.
</Note>

### Properties

<ResponseField name="lap_number" type="int">
  The lap number (1-indexed). Access via `lap.lap_number` property.
</ResponseField>

<ResponseField name="driver" type="str">
  The 3-letter driver code. Access via `lap.driver` property.
</ResponseField>

<ResponseField name="telemetry" type="DataFrame">
  High-frequency telemetry data for this lap. Loaded lazily on first access.
  Contains Time, Speed, RPM, Throttle, Brake, nGear, DRS, and position data.
</ResponseField>

<ResponseField name="session" type="Session">
  Reference to the parent session object.
</ResponseField>

### Series Data

Since `Lap` extends `pd.Series`, lap timing data is accessed via dictionary-style indexing or the `get()` method:

* `lap.get('LapTime')`: Lap time as timedelta
* `lap.get('LapTimeSeconds')`: Lap time in seconds as float
* `lap.get('LapNumber')`: Lap number (same as `lap.lap_number`)
* `lap.get('Driver')`: Driver code (same as `lap.driver`)
* `lap.get('Sector1Time')`, `lap.get('Sector2Time')`, `lap.get('Sector3Time')`: Sector times
* `lap.get('Compound')`: Tire compound
* `lap.get('TyreLife')`: Tire age in laps
* `lap.get('IsPersonalBest')`: Boolean indicating personal best lap

### Methods

#### `get_telemetry()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_telemetry() -> DataFrame
```

Explicitly load telemetry data. Same as accessing the `telemetry` property.

**Returns:**

* DataFrame with telemetry samples

**Example:**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
lap = ver.get_lap(19)
tel = lap.get_telemetry()

# Analyze telemetry
print(f"Samples: {len(tel)}")
print(f"Max speed: {tel['Speed'].max()} km/h")
print(f"Max RPM: {tel['RPM'].max()}")
```

#### `get_car_data()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_car_data(**kwargs) -> DataFrame
```

Get car telemetry data (Speed, RPM, Throttle, Brake, nGear, DRS).

**Returns:**

* DataFrame with car telemetry channels

#### `get_pos_data()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_pos_data(**kwargs) -> DataFrame
```

Get position data (X, Y, Z coordinates).

**Returns:**

* DataFrame with position data

#### `get_weather_data()`

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_weather_data() -> DataFrame
```

Get weather data for this lap.

**Returns:**

* DataFrame with weather information

***

## Complete Examples

### Basic session usage

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import tif1

# Load session
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")

# Access drivers
print(f"Drivers: {session.drivers}")

# Get all laps
laps = session.laps
print(f"Total laps: {len(laps)}")

# Get fastest laps
fastest = session.get_fastest_laps(by_driver=True)
print(fastest[["Driver", "LapTimeSeconds", "Compound"]].head())
```

### Working with Drivers

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Get specific driver
ver = session.get_driver("VER")
print(f"Driver: {ver.driver}")
print(f"Team: {ver['TeamName']}")  # Access Series data
print(f"Number: {ver['DriverNumber']}")  # Access Series data

# Get driver's laps
ver_laps = ver.laps
print(f"Laps completed: {len(ver_laps)}")

# Get fastest lap
fastest = ver.get_fastest_lap()
print(f"Fastest: {fastest['LapTimeSeconds'].iloc[0]:.3f}s")
```

### Working with Telemetry

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Get fastest lap telemetry
tel = session.get_fastest_lap_tel()

# Analyze telemetry
# Available columns: Time, Speed, RPM, Throttle, Brake, nGear, DRS, X, Y, Z, Distance
print(f"Max speed: {tel['Speed'].max():.1f} km/h")
print(f"Max RPM: {tel['RPM'].max()}")
print(f"Avg throttle: {tel['Throttle'].mean():.1f}%")

# Find braking zones
braking = tel[tel['Brake'] == True]
print(f"Braking points: {len(braking)}")

# Plot speed trace
import matplotlib.pyplot as plt
plt.plot(tel['Distance'], tel['Speed'])
plt.xlabel('Distance (m)')
plt.ylabel('Speed (km/h)')
plt.show()
```

<Note>
  **Available Telemetry Columns:**

  * `Time`: Timestamp (timedelta)
  * `Speed`: Speed in km/h
  * `RPM`: Engine RPM
  * `Throttle`: Throttle position (0-100%)
  * `Brake`: Brake status (boolean or 0-100%)
  * `nGear`: Gear number
  * `DRS`: DRS status
  * `X`, `Y`, `Z`: 3D position coordinates (meters)
  * `Distance`: Distance along track (meters)
</Note>

### Parallel data loading

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import tif1

async def load_session():
    session = tif1.get_session(2021, "Belgian Grand Prix", "Race")

    # Load laps in parallel
    laps = await session.laps_async()

    # Get telemetry for multiple drivers in parallel
    tels = await session.get_fastest_laps_tels_async(
        by_driver=True,
        drivers=["VER", "HAM", "BOT"]
    )

    for driver, tel in tels.items():
        print(f"{driver}: {tel['Speed'].max():.1f} km/h")

asyncio.run(load_session())
```

<Tip>
  **Performance Tip:** `laps_async()` is significantly faster than the `laps` property for cold starts
  because it fetches all driver lap data in parallel. Use async methods when performance is critical.
</Tip>

### Ultra-cold start mode

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Enable ultra-cold start for minimal latency
import tif1

# Configure ultra-cold mode globally
config = tif1.get_config()
config.ultra_cold = True

session = tif1.get_session(2021, "Belgian Grand Prix", "Qualifying")

# Get fastest lap telemetry with ultra-cold optimization
tel = session.get_fastest_lap_tel(ultra_cold=True)

# This skips loading all laps and goes directly to the fastest lap
print(f"Loaded in minimal time: {len(tel)} samples")
```

<Note>
  **Ultra-cold mode** bypasses cache validation and skips loading unnecessary data for faster cold starts.
  Use when you need minimal latency and don't need all session data. The trade-off is that some
  data validation is skipped.
</Note>

### Error Handling

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import tif1
from tif1.exceptions import DriverNotFoundError, LapNotFoundError, DataNotFoundError

try:
    session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
    driver = session.get_driver("VER")
    lap = driver.get_lap(1)
    tel = lap.telemetry
    print(f"Lap 1 telemetry: {len(tel)} samples")
except DriverNotFoundError as e:
    print(f"Driver not found: {e}")
except LapNotFoundError as e:
    print(f"Lap not found: {e}")
except DataNotFoundError as e:
    print(f"Session data not available: {e}")
```

### Performance Comparison

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import time
import asyncio
import tif1

# Sync approach (slower)
start = time.time()
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
laps = session.laps  # Sequential loading
print(f"Sync: {time.time() - start:.2f}s")

# Async approach (faster)
async def load_async():
    start = time.time()
    session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
    laps = await session.laps_async()  # Parallel loading
    print(f"Async: {time.time() - start:.2f}s")

asyncio.run(load_async())
```

<Tip>
  **Best Practices:**

  * Use async methods (`laps_async()`, `get_fastest_laps_tels_async()`) for cold starts
  * Enable caching to speed up subsequent loads
  * Use `ultra_cold=True` when you only need specific data
  * Filter drivers with the `drivers` parameter to reduce data transfer
</Tip>

***

## Troubleshooting

### Common Issues

**Issue: `DriverNotFoundError` when using driver codes**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# ❌ Wrong - drivers property returns numbers, not codes
drivers = session.drivers  # Returns ["1", "33", "44"]
driver = session.get_driver(drivers[0])  # May fail if "1" is not a valid code

# ✅ Correct - use drivers_df to get codes
drivers_df = session.drivers_df
driver_code = drivers_df.iloc[0]["Abbreviation"]  # Returns "VER"
driver = session.get_driver(driver_code)

# ✅ Also correct - get_driver() accepts both codes and numbers
driver = session.get_driver("VER")  # By code
driver = session.get_driver(33)     # By number
```

**Issue: `LapNotFoundError` when accessing laps**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# ❌ Wrong - lap numbers start at 1, not 0
lap = driver.get_lap(0)  # Raises LapNotFoundError

# ✅ Correct - use 1-indexed lap numbers
lap = driver.get_lap(1)  # First lap
```

**Issue: Empty telemetry data**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Check if telemetry is available
tel = lap.get_telemetry()
if len(tel) == 0:
    print("No telemetry data available for this lap")
else:
    print(f"Telemetry loaded: {len(tel)} samples")
```

**Issue: Slow data loading**

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# ❌ Slow - sequential loading
for driver_code in ["VER", "HAM", "BOT"]:
    driver = session.get_driver(driver_code)
    tel = driver.get_fastest_lap_tel()

# ✅ Fast - parallel loading
tels = session.get_fastest_laps_tels(
    by_driver=True,
    drivers=["VER", "HAM", "BOT"]
)
```

***

## Related Pages

<CardGroup cols={2}>
  <Card title="API Overview" href="/api-reference/overview">
    Complete API map
  </Card>

  <Card title="Models" href="/api-reference/models">
    Data structures
  </Card>

  <Card title="Sessions Concept" href="/concepts/sessions">
    Understanding sessions
  </Card>

  <Card title="Getting Started" href="/getting-started">
    Usage guide
  </Card>
</CardGroup>
