Skip to content

About

Official Python SDK for the SchoolDigger K-12 School Data API

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

SchoolDigger Python SDK

Official Python client for the SchoolDigger API — K-12 school data for 120,000+ U.S. schools.

Installation

pip install schooldigger

Quick Start

import schooldigger

config = schooldigger.Configuration()
config.api_key["appID"] = "YOUR_APP_ID"
config.api_key["appKey"] = "YOUR_APP_KEY"

client = schooldigger.ApiClient(config)

# Search schools
api = schooldigger.SchoolsApi(client)
result = api.search_schools(st="WA", q="Lincoln")
for school in result.school_list:
    print(f"{school.school_name} — {school.address.city}, {school.address.state}")

# Get a specific school by SchoolDigger ID
school = api.get_school("530966001632")
print(f"Enrollment: {school.school_yearly_details[0].number_of_students}")

API Classes

Class Methods
SchoolsApi search_schools(), get_school()
DistrictsApi search_districts(), get_district()
RankingsApi get_school_rankings(), get_district_rankings()
AutocompleteApi autocomplete_schools(), autocomplete_districts()
schooldigger.batch.BatchClient submit_batch(), get_batch(), get_batch_results(), wait_for_batch(), cancel_batch() (see Batch requests)

Test scores in API 3.0

Pass include_ranges=True to get_school / get_district (without it, test scores are as in API 2.4). Each test-score percent (percent_met_standard, percent_tier1 ... percent_tier5) is either a number or an APIReportedPercent object (status of range, suppressed, legacyImputed or derived, with optional value, low, high), and is None when the state reported nothing. The SDK exposes the union through .actual_instance:

school = api.get_school("330004800609", include_ranges=True)
for ts in school.test_scores or []:
    v = ts.school_test_score.percent_met_standard if ts.school_test_score else None
    v = v.actual_instance if v is not None else None
    if v is None:
        text = "not reported"
    elif isinstance(v, (int, float)):
        text = f"{v}%"
    elif v.status == "suppressed":
        text = "suppressed by the state"
    elif v.low is not None and v.low <= 0:
        text = f"< {v.high}%"
    elif v.high is not None and v.high >= 100:
        text = f">= {v.low}%"
    elif v.low is not None:
        text = f"{v.low}-{v.high}%"
    else:
        text = f"{v.value}%"
    print(ts.year, ts.grade, ts.subject, text)

See Data Quality and Suppression for the full vocabulary and rendering rule. API 2.4 and below (SDK 1.x) return only exact values.

Low-income measure in API 3.1

SDK 3.0 targets API 3.1. School records from get_school, search_schools and get_school_rankings carry low_income: the school's one low-income measure, type "FRL" (free/reduced-price lunch eligible), "DC" (directly certified for free meals; used where the school reports no usable free/reduced count) or "CEP" (likely serves free meals to all students under the Community Eligibility Provision; no percent), with year, percent, count, enrollment, community_eligibility_likely and statewide_free_meals. It is None when the school has no usable value. FRL and DC are not comparable: don't sort one against the other.

school = api.get_school("250309000385")
li = school.low_income
if li is None:
    print("no low-income figure")
elif li.type == "CEP":
    print(f"All students receive free meals (Community Eligibility, {li.year - 1}-{li.year % 100:02d})")
else:
    label = "Free/reduced lunch eligible" if li.type == "FRL" else "Directly certified for free meals"
    print(f"{label}: {li.percent}% ({li.year - 1}-{li.year % 100:02d})")
for y in school.school_yearly_details or []:
    print(y.year, y.free_reduced_lunch, y.direct_certification)

Each yearly detail has free_reduced_lunch (count and percent, or a status of communityEligibility, repeated or aboveEnrollment) and direct_certification. SDK 3.0 renames the school models for API 3.1, a breaking change for code that imports them by name (APISchoolFull31Ranges, APISchoolList31, APISchoolSummary31, APISchoolListRank31, APISchoolSummaryRank31). See Low-income measures.

Vaccination rates in API 3.1

SDK 3.1 adds vaccination to get_school: the state's published vaccination rates for the school, with source (agency, url, headline_label, suppression_note, district_scope), headline (the state's lead figure, e.g. kindergartners with all required vaccines: year, grade_name, antigen, status, label, value, students, students_total, exemption_rate), district_name / county_name / state_name, years_available, grades_available and figures (each year, grade, vaccine and status, with the school's value and the state's own district, county and state values). By default figures holds the headline grade for its most recent year; pass include_vaccination_detail=True for every grade and year. search_schools carries the headline alone on each school as vaccination. Both are None when the state publishes no vaccination data for the school, so check before use.

Each value has a status: "exact" (pct), "range" (range_low / range_high), "suppressed", "redactedCount", "countOnly" (see students) or "invalid".

school = api.get_school("530111000200", include_vaccination_detail=True)
v = school.vaccination
if v is None or v.headline is None or v.headline.value is None:
    print("no vaccination headline")
else:
    h, val = v.headline, v.headline.value
    if val.status == "exact":
        text = f"{val.pct}%"
    elif val.status == "range":
        text = f"{val.range_low}-{val.range_high}%"
    else:
        text = val.status
    print(f"{h.label} ({h.year - 1}-{h.year % 100:02d}): {text}  [{v.source.agency}]")
    print(f"{len(v.figures)} figures for years {v.years_available}")

District endpoints are unchanged. See Vaccination rates.

Batch requests (SDK 3.2)

The async batch API runs up to 2,000 API GET calls for you, paced at half your plan's per-minute limit, and returns the results in parts of 20. Each item counts as one API call; submitting, polling and downloading are free. Paid plans only. schooldigger.batch wraps it (import the submodule; it isn't in the package root):

from schooldigger.batch import BatchClient, BatchError

client = BatchClient("YOUR_APP_ID", "YOUR_APP_KEY")   # or BatchClient(configuration=config)
batch = client.submit_batch(
    [{"id": sid, "path": f"/v3.1/schools/{sid}"} for sid in ["530792001309", "530792003116"]]
)                                                     # plain path strings work too
for part in client.wait_for_batch(batch["batchId"]):  # polls with Retry-After, yields parts as ready
    for r in part.results:                            # {"id", "path", "status", "body"}
        print(r["id"], r["status"], (r["body"] or {}).get("schoolName"))
Method Route
submit_batch(items, callback_url=None) POST /batches (returns batchId, partCount, ...)
get_batch(batch_id) GET /batches/{batchId} (status, partsReady, ...)
get_batch_results(batch_id, part) GET /batches/{batchId}/results?part=N, decoded NDJSON
cancel_batch(batch_id) DELETE /batches/{batchId}
wait_for_batch(batch_id, timeout=None) polls and yields BatchPart(part, results); raises BatchNotCompletedError if the batch ends failed, canceled or expired

body is the plain JSON the same GET returns (a dict, not a model). Error responses raise BatchError with status, message, description and, for a rejected submit, errors. To check a callback's X-SchoolDigger-Signature header, pass the raw request body to verify_signature(raw_body, header, app_key).

Authentication

All API calls require an appID and appKey. Get your free API key at developer.schooldigger.com.

Documentation

Plans

Plan Price Highlights
DEV/TEST Free Enterprise-level access, 20 calls/day
Basic $19.90/mo 1 year of data
Pro $89/mo Boundaries, geo search, finance
Enterprise $189/mo Full depth, boundary containment

License

MIT

About

Official Python SDK for the SchoolDigger K-12 School Data API

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages