Calculating California Public Records Act Deadlines in Python
Within State Law Compliance Frameworks, the California Public Records Act (CPRA) is the jurisdiction that most often trips automated deadline math, because its determination window is counted in calendar days while the two other large state acts a records team routinely handles — the Texas Public Information Act and New York’s FOIL — run on business days. Getting that one distinction wrong produces a clock that is off by several days in exactly the direction that causes a missed statutory deadline. This page builds a small, dependency-free Python module that computes the CPRA determination deadline correctly, applies the 14-day unusual-circumstances extension, rolls a due date off a weekend or state holiday, and writes an audit-ready record for every calculation.
The CPRA Statutory Clock
Under California Government Code § 7922.535(a), an agency that receives a request for records must, within 10 calendar days, determine whether the request seeks disclosable records in its possession and notify the requester of that determination and the reasons for it. The determination window is not a deadline to produce the records; it is a deadline to decide and communicate. Production follows “promptly” once the agency has made its determination. That two-step structure is the first thing an automation must model, because a system that treats “day 10” as the delivery date will misreport the agency’s posture to every requester.
The 10-day count runs on calendar days, so weekends and holidays inside the window do not pause it. Section 7922.535(b) permits an agency, in “unusual circumstances,” to extend the determination window by written notice for up to 14 additional calendar days — a maximum of 24 calendar days from receipt. Unusual circumstances are narrowly defined (records held offsite, voluminous separate-and-distinct records, or the need to consult another agency), and the extension must be justified in writing, so the automation should treat the extension as an explicit, logged decision rather than a default.
One rule keeps the calculation honest even though it is calendar-day based: when the computed determination date lands on a Saturday, Sunday, or state holiday — a day the agency is not open to receive or act on the determination — the deadline rolls forward to the next business day. Modeling that roll-forward is where a holiday calendar re-enters an otherwise calendar-day calculation. Escalation and reminder logic downstream in Deadline Tracking & Escalation depends on this record being both correct and explainable.
Prerequisites & Environment Setup
The module is pure standard library so it can run inline in the request path with no third-party runtime dependency and no supply-chain surface.
- Python 3.11+ — for
datetime.date,frozenset[date]typing, anddataclass(frozen=True, slots=True). - Standard library only:
dataclasses,datetime,json,logging. No external calendar library is required; the state holiday set is a small, reviewable constant. - A version-controlled holiday set — California’s statutory holidays under Gov. Code § 6700 change little year to year, but you should pin one
frozensetper calendar year and review it against the published schedule, exactly as you would any compliance artifact. - A structured log sink — stdout JSON in development, forwarded to your append-only store or SIEM in production so every computed deadline is reconstructable on appeal.
Keep the holiday set beside the code under version control. A deadline miscomputed because a holiday was silently dropped is indistinguishable, on audit, from an agency that simply ignored the statute.
Modeling the Determination Window
The diagram below shows the single request as two possible clocks: the standard 10-calendar-day determination window and the extended 24-day window when § 7922.535(b) applies, with the weekend/holiday roll-over noted underneath.
The base determination date and the rolled determination date are recorded separately in the module below, so an auditor can see both the raw statutory count and the day the agency was actually obligated to act.
Step-by-Step Implementation
The module defines a frozen result record, a state holiday set, a roll-forward helper, and the deadline function itself. The comments cite the specific subsection each line implements.
from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone
class JsonAuditFormatter(logging.Formatter):
"""Emit one structured JSON object per audit event (NIST SP 800-53 AU-3)."""
def format(self, record: logging.LogRecord) -> str:
payload = {
"ts": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"event": record.getMessage(),
}
for key in ("request_id", "statute", "received", "deadline"):
if hasattr(record, key):
payload[key] = getattr(record, key)
return json.dumps(payload)
logger = logging.getLogger("cpra_deadline")
logger.setLevel(logging.INFO)
_handler = logging.StreamHandler()
_handler.setFormatter(JsonAuditFormatter())
logger.addHandler(_handler)
# California state holidays observed under Cal. Gov. Code § 6700 (2026 instances).
# The CPRA determination window is counted in *calendar* days, but a deadline that
# lands on one of these days (or a weekend) rolls forward to the next business day.
CA_STATE_HOLIDAYS_2026: frozenset[date] = frozenset({
date(2026, 1, 1), # New Year's Day
date(2026, 1, 19), # Dr. Martin Luther King Jr. Day
date(2026, 2, 16), # Presidents' Day
date(2026, 3, 31), # Cesar Chavez Day — § 6700(a)(16)
date(2026, 5, 25), # Memorial Day
date(2026, 7, 3), # Independence Day (observed)
date(2026, 9, 7), # Labor Day
date(2026, 11, 11), # Veterans Day
date(2026, 11, 26), # Thanksgiving Day
date(2026, 11, 27), # Day after Thanksgiving
date(2026, 12, 25), # Christmas Day
})
@dataclass(frozen=True, slots=True)
class CpraDeadline:
"""Immutable record of one CPRA determination calculation."""
request_id: str
received: date
statute: str
base_determination: date # raw calendar-day count, before roll-forward
unusual_circumstances: bool
determination_due: date # the day the agency is actually obligated to act
def next_business_day(day: date, holidays: frozenset[date]) -> date:
"""Advance to the next weekday that is not a state holiday."""
while day.weekday() >= 5 or day in holidays:
day += timedelta(days=1)
return day
def cpra_determination_deadline(
request_id: str,
received: date,
holidays: frozenset[date] = CA_STATE_HOLIDAYS_2026,
unusual_circumstances: bool = False,
) -> CpraDeadline:
"""Compute the CPRA determination deadline for a single request.
Cal. Gov. Code § 7922.535(a): the agency must determine whether the request
seeks disclosable records within 10 *calendar* days of receipt.
§ 7922.535(b): unusual circumstances may extend that window by up to 14 days.
A deadline falling on a weekend or § 6700 holiday rolls to the next business day.
"""
try:
if not isinstance(received, date):
raise TypeError("received must be a datetime.date")
# 10 calendar days from receipt — NOT business days (contrast TX PIA / NY FOIL).
base = received + timedelta(days=10)
deadline = base
if unusual_circumstances:
# § 7922.535(b): up to 14 additional calendar days, by written notice.
deadline = deadline + timedelta(days=14)
# Roll a weekend/holiday landing forward; the clock never expires on a closed day.
determination_due = next_business_day(deadline, holidays)
record = CpraDeadline(
request_id=request_id,
received=received,
statute="Cal. Gov. Code § 7922.535",
base_determination=base,
unusual_circumstances=unusual_circumstances,
determination_due=determination_due,
)
logger.info(
"cpra_deadline_computed",
extra={
"request_id": request_id,
"statute": record.statute,
"received": received.isoformat(),
"deadline": determination_due.isoformat(),
},
)
return record
except Exception:
# Fail loud: a miscomputed statutory clock is itself a CPRA violation.
logger.error("cpra_deadline_failed", extra={"request_id": request_id})
raise
The function returns a structured record rather than a bare date because downstream reminder, escalation, and audit logic all need the surrounding context — the raw count, the roll-forward, and whether the extension was invoked — not just the final day.
Validation & Verification
Assert the two conventions that most often drift: the calendar-day base count, and the roll-forward off a closed day. The example below uses a request received Thursday, June 25, 2026, whose base determination lands on Sunday, July 5.
if __name__ == "__main__":
standard = cpra_determination_deadline("CA-2026-0142", date(2026, 6, 25))
# Base determination 2026-07-05 falls on a Sunday, so it rolls to Monday 2026-07-06.
assert standard.base_determination == date(2026, 7, 5)
assert standard.determination_due == date(2026, 7, 6)
extended = cpra_determination_deadline(
"CA-2026-0142", date(2026, 6, 25), unusual_circumstances=True
)
# § 7922.535(b): +14 calendar days -> 2026-07-19 (Sunday) rolls to 2026-07-20.
assert extended.determination_due == date(2026, 7, 20)
print("CPRA deadline checks passed")
Because CpraDeadline is frozen, replaying the same request during recovery yields an identical record and never starts a second statutory clock — the idempotency property an auditor expects. For broader confidence, replay a corpus of historical requests spanning each holiday and diff the computed deadlines against a manually verified table before any production deploy. The append-only trail this produces feeds naturally into the Audit Logging Architecture that oversight bodies read.
Troubleshooting & Edge Cases
- Treating day 10 as the delivery date. The 10-day window is a determination deadline, not a production deadline. Diagnosis: requesters told records will arrive on day 10 that instead arrive later. Fix: model the determination and the “prompt” production as distinct events; only the determination is fixed by § 7922.535(a).
- Counting the determination window in business days. Applying business-day math to the CPRA window overstates the deadline by two or more days. Diagnosis: California deadlines that mysteriously match the Texas or New York figures. Fix: the base count is calendar days; business days re-enter only for the roll-forward. The distinction is drawn explicitly against the Texas Public Information Act deadline model and the New York FOIL deadline model.
- Applying the extension by default. The 14-day extension requires written notice and a qualifying unusual circumstance. Diagnosis: every request quietly carrying a 24-day window. Fix: require
unusual_circumstances=Trueto be set by an explicit, logged decision, never inferred. - A holiday missing from the set. If Cesar Chavez Day or another § 6700 observance is absent, a deadline can land on a day the agency is closed. Diagnosis: a determination due date one business day early. Fix: pin and review the holiday
frozensetper year; assert coverage in tests. - Ambiguous receipt date. A request received after business hours or over a weekend may have a receipt date the agency logs differently than the requester expects. Fix: normalize the receipt date upstream at intake and treat it as authoritative for the clock.
Compliance Verification Checklist
FAQ
Does the California Public Records Act count calendar days or business days?
The 10-day determination window under Gov. Code § 7922.535(a) is counted in calendar days — weekends and holidays inside the window do not pause it. Business days re-enter only at the edge: if the computed determination date lands on a Saturday, Sunday, or state holiday, it rolls forward to the next business day. This is the opposite convention from the Texas Public Information Act and New York FOIL, which count the window itself in business days, and mixing the two is the most common CPRA deadline bug.
How long is the unusual-circumstances extension, and when does it apply?
Section 7922.535(b) allows up to 14 additional calendar days, for a maximum of 24 calendar days from receipt. It applies only in the narrow situations the statute lists — records stored offsite, voluminous separate-and-distinct records, or the need to consult another agency — and the agency must give the requester written notice of the extension and its basis. Because it is a justified, communicated decision, the module requires it to be set explicitly rather than defaulting it on.
What happens if the determination deadline falls on a state holiday?
It rolls forward to the next day the agency is open. The next_business_day helper skips weekends and any date in the § 6700 holiday set, so a base determination on, say, Cesar Chavez Day advances to the following business day. Keeping that holiday set accurate and version-controlled is essential: a missing observance produces a deadline one business day early, which reads on audit as an agency acting outside its statutory window.
Related
- State Law Compliance Frameworks — the jurisdiction-rule layer this calculation plugs into
- Calculating Texas Public Information Act Deadlines — the business-day, Attorney-General-tolling counterpart
- Calculating New York FOIL Deadlines — the 5-business-day acknowledgment counterpart
- Deadline Tracking & Escalation — consumes these deadline records to drive reminders and escalation
- Computing Business-Day FOIA Deadlines with Federal Holiday Calendars — the general business-day engine behind the roll-forward