Variance & Exception Handling

A rule engine that only knows the code will be wrong about every parcel that has ever been granted relief from it. Variances, legal nonconforming status and development agreements are facts about individual properties, each with a date and a document behind it, and they beat the general rule by design. This module of Rule Engine Design for Zoning & Setback Automation covers how to model them so that an override is always granted, never inferred, and always visible in the verdict it produced.

How a granted exception reaches a verdictApplicable rules are assembled, active overrides for the parcel and date are looked up, the effect is applied by kind, and the verdict names the governing authority.Assemble the applicable rulesby district and effective dateLook up active overrides for this parcelkeyed by parcel; bounded by dateApply by effect, not by assumptionsubstitute a value, exempt, or freeze the rule dateEvaluate anyway, and report the measurementan exempt rule still yields the number a reviewer needsName the governing authority in the verdictinstrument, granting body, conditions
Overrides are applied at evaluation, never written into the rule pack — so the code stays readable as the code.

Prerequisites

  • A rule store where general standards live, separate from wherever overrides will live. Mixing them is the mistake this module exists to prevent.
  • A stable parcel identifier that survives refreshes, since an override is attached to a property rather than to a row.
  • Access to the instruments themselves — the variance resolution, the certificate of nonconformity, the development agreement — or at minimum their citations and dates.
  • Effective-date handling already working in the engine, because almost every override is bounded in time or scope.
  • A verdict record that can name its governing authority, so an override-driven outcome can say so.

Three Kinds of Exception, Three Different Shapes

They are frequently lumped together and they behave differently, which matters because the differences show up in the data model.

Three exceptions that behave differentlyA variance substitutes a threshold, a legal nonconforming status exempts an existing condition, and a development agreement can freeze the applicable code.ChangesModelled asVarianceOne threshold, for one parcelsubstitute_value, with conditionsLegal nonconformingWhether the rule reaches the existing conditionexempt — new work still evaluatedDevelopmentagreementMany standards, often the effective date itselffreeze_rule_date, over a termDiscretionary approvalNothing a pipeline can evaluateA review trigger, not an override
Model all three as threshold substitutions and the nonconforming case will excuse new work it should not.

A variance is relief granted from a specific standard for a specific parcel, usually with conditions and often with an expiry or a build-by date. It changes one threshold and leaves the rest of the code applying. It is the narrowest and the easiest to model: a per-parcel, per-rule substitution.

A legal nonconforming status — grandfathering — is not relief at all. It says the structure predates the standard and may continue, typically until it is substantially altered or destroyed. It does not change a threshold; it changes whether the rule applies to the existing condition, while still applying to new work. Modelling it as a threshold override produces the wrong answer for an addition.

A development agreement is a negotiated instrument that can modify many standards at once, often for a term of years, and sometimes freezes the applicable code as of its execution date. That last property is the interesting one: it is an effective-date override rather than a threshold override, and an engine that only knows how to substitute values cannot express it.

The Override Record

Whatever the kind, the record needs the same core: what it applies to, what it changes, when it is in force, and what authorised it.

from dataclasses import dataclass

@dataclass(frozen=True)
class Override:
    """A granted exception. Never inferred — every field traces to an instrument."""
    parcel_id: str
    kind: str                  # "variance" | "nonconforming" | "agreement"
    rule_id: str | None        # None for an agreement that freezes the whole pack
    effect: str                # "substitute_value" | "exempt" | "freeze_rule_date"
    value: float | None        # the substituted threshold, where applicable
    unit: str | None
    effective_from: str
    effective_to: str | None   # None = until superseded
    instrument: str            # "Var. 2024-118" — the document
    granted_by: str            # the body that granted it
    conditions: tuple = ()     # e.g. "build-by 2027-06-30", "no further subdivision"

Three fields do the heavy lifting. effect distinguishes the three kinds structurally, so the engine cannot apply a nonconforming status as if it were a threshold change. instrument makes the override attributable, and an override without one should be rejected at load. And conditions are recorded even though the engine mostly cannot evaluate them, because a condition the pipeline cannot check is exactly the thing a reviewer needs to see.

Scope: What an Override Actually Covers

An override’s scope is narrower than people assume, and getting it wrong in either direction produces a wrong answer that looks reasonable.

A variance granted for a front setback does not excuse a side setback, a height breach or a density overrun, even though all four appear on the same application. Modelling an override as “this parcel is excused” rather than “this rule, for this parcel, takes this value” is the single most common modelling error here, and it produces a parcel that passes everything.

Scope is also bounded by what the relief was granted for. A variance granted to permit a specific structure at a specific location generally does not travel to a different structure built later in the same place, and a well-drafted instrument says so. Where the instrument names a project, the override record should too, and a verdict relying on it should state which project it was granted for — which lets a reviewer notice when the application in front of them is a different one.

Successors are the third dimension. Some relief runs with the land and binds future owners; some is personal to the applicant and lapses on transfer. The distinction is legal and jurisdiction-specific, and the pipeline’s role is to record which kind an instrument granted rather than to infer it. Where the instrument is silent and local practice is unsettled, the honest treatment is a flag rather than an assumption.

All three dimensions — which rule, which project, which successors — fit as fields on the override record and cost nothing to carry. They are also exactly the fields somebody will ask about, which is a reliable signal that they belong in the record rather than in institutional memory.

Applying an Override Without Corrupting the Rule Set

The temptation is to write overrides into the rule pack — a per-parcel exception list inside the R-2 rules. It is the single change that does the most damage, because it makes the regulatory standard unreadable and lets an override be edited by someone reviewing regulation.

Why overrides live in their own storeSeparating the rule pack from the override store keeps regulation reviewable as regulation and case history reviewable as case history.Rule pack: the code, as adoptedreviewed by planning; diffed on amendmentOverride store: what was granted, to whomreviewed as case history; append-onlyApplied at evaluation, by lookupnever by editing either storeVerdict names which governedand the instrument that authorised it
Writing per-parcel exceptions into a rule pack makes the code unreadable and lets an override be edited by someone reviewing regulation.

Overrides live in their own store, keyed by parcel, and are applied at evaluation time as a lookup after the applicable rule set is assembled. The rule pack stays a clean statement of the code; the override store stays a clean record of case history; and the verdict names which one governed.

def apply_overrides(rules, overrides, parcel_id, as_of):
    """Overrides beat the general rule, but only where one was actually granted."""
    active = [o for o in overrides.get(parcel_id, [])
              if o.effective_from <= as_of and (o.effective_to is None or as_of <= o.effective_to)]
    resolved = []
    for rule in rules:
        match = next((o for o in active if o.rule_id == rule.id), None)
        if match is None:
            resolved.append((rule, None))
        elif match.effect == "exempt":
            resolved.append((rule, match))        # evaluated, but reported as exempt
        elif match.effect == "substitute_value":
            resolved.append((rule.with_value(match.value, match.unit), match))
        else:                                      # freeze_rule_date and friends
            resolved.append((rule, match))
    return resolved

Notice that an exempt rule is still evaluated. Skipping it loses the measurement, and the measurement is what a reviewer needs to decide whether the exemption still applies — a nonconforming structure that has grown is no longer covered by its nonconforming status, and only the measurement reveals that.

Overrides in the Report

An override changes an outcome, which makes it the single most important thing a report can explain — and the thing most likely to be omitted because the outcome looks unremarkable.

A verdict reached under an override should say so on its face: the standard that would otherwise apply, the value or exemption granted, the instrument that granted it, the body that granted it, the conditions attached, and the expiry if there is one. That is six short fields and it turns a compliant verdict that a neighbour disputes into a document that answers the dispute without anybody opening a filing cabinet.

The same applies in aggregate. A report summarising a run should count verdicts reached under override separately from those reached under the code, because they are different claims. A jurisdiction where a fifth of the passes depend on granted relief is in a materially different position from one where none do, and only a report that distinguishes them can show it.

Where an override was found but not applied — expired, out of scope, granted for a different project — say that too. The absence of an expected override is exactly what an applicant will query, and recording the reason converts a phone call into a line in the document.

Expiry, Conditions and Silent Lapse

Overrides lapse, and an engine that never re-checks will apply a variance that expired two years ago.

Expiry is the easy half: an effective-to date, checked against the application date, with the override simply falling out of the active set afterwards. What deserves attention is the approaching expiry, because a variance with a build-by date that has not been exercised will lapse, and the affected applicant would rather know now. A scheduled query over the override store — overrides expiring within ninety days, with their parcels — costs nothing and is genuinely useful to a planning office.

Conditions are the harder half, because most of them are not machine-checkable. “Subject to installation of screening” is a fact about the world, not about the data. The correct behaviour is to record the condition, apply the override, and mark the verdict as conditional so the report says plainly that the outcome depends on a condition the pipeline did not verify. Pretending the condition is satisfied, or refusing to apply the override at all, are both worse than saying which it is.

Nonconforming status lapses differently: it is usually extinguished by substantial alteration or by a period of discontinued use. Neither is visible in a parcel layer. Where the pipeline can detect a trigger — a footprint that has grown materially since the status was granted — it should flag it for review rather than deciding, since “substantial” is a defined term with a threshold that varies by code.

Getting Existing Exceptions Into the System

Most organisations adopting a rule engine already have decades of granted relief sitting in resolutions, minutes and filing cabinets, and the honest position is that not all of it will be captured.

The pragmatic sequence starts with recency and materiality rather than completeness. Overrides granted in the last few years are the ones most likely to still be in force and most likely to be relied on; those are worth entering first. Overrides against rules the pipeline actually evaluates matter more than ones against standards it does not model. And parcels with active applications matter more than dormant ones.

What matters far more than coverage is knowing the coverage. A pipeline that has 60% of historical variances entered and says so is usable: verdicts on parcels with no recorded override carry a caveat that relief may exist and not be captured, which is exactly what a reviewer needs to know. A pipeline that has 60% and presents itself as complete produces confident violations against parcels that were legally excused, which is the outcome that destroys trust in the system.

Practically, that means an explicit coverage statement per jurisdiction — “variances entered from 2015; earlier relief not captured” — carried into the report footer, and a mechanism for a reviewer to add a missing override when they find one, with the instrument attached. The second is what turns the backlog into something that shrinks through use rather than through a project.

There is a related trap in bulk entry. Overrides transcribed from resolutions without the conditions attached are half-records, and a variance granted subject to conditions is not the same as one granted outright. Where the conditions cannot be transcribed reliably, recording their existence — “conditions apply, see instrument” — is much better than recording an unconditional override that the pipeline will apply without qualification.

It also gives planning staff a reason to keep the store current, which no amount of engineering discipline achieves on its own.

Auditing the Override Store

Because overrides beat the code, the override store is the highest-value target in the system for both error and mischief, and it deserves proportionate treatment.

Controls the override store earnsEvery override references an instrument, changes are append-only, write access is separate from the rule pack, and the store is reconciled against the planning register.An instrument, validated at loadno instrument, no entryAppend-only, superseded not editedthe history of what was granted survivesSeparate write access from the rule packdifferent approvers, different processReconciled against the planning registerdifferences reported in both directionsUsage reported as an ordinary outputa rule overridden a third of the time is worth revisiting
Overrides beat the code, which makes this the highest-value store in the system for both error and mischief.

Three controls are worth having from the start. Every override references an instrument, and the reference is validated at load; an override that cannot name its authority does not enter the store. Changes are append-only, with supersession rather than editing, so the history of what was granted survives. And write access is separate from rule-pack write access, because the two are approved by different people through different processes.

Reconciliation closes the loop: periodically compare the override store against the source of truth — the planning department’s own variance register — and report differences in both directions. Overrides in the pipeline with no matching resolution are the serious finding; resolutions with no matching override are a coverage gap, which is usually more common and less alarming.

Report override usage as an ordinary output too. A run that applied 140 overrides is telling a planner something about their jurisdiction, and a rule that is overridden on a third of the parcels it touches is a rule worth revisiting — which is a policy insight the pipeline is well placed to surface and that nobody would otherwise assemble.

Reported that way, the override store stops being an implementation detail and becomes a visible part of how the jurisdiction actually regulates — which is usually the first time anyone has been able to see it in aggregate.

Troubleshooting

  • A parcel passes that a planner expects to fail. Check the active override set first; an expired override still being applied means the as-of date is the run date rather than the application date.
  • An override applies after a subdivision. The parcel it was granted for no longer exists. Overrides need the same lineage treatment as compliance history: carried forward deliberately or closed, never inherited by default.
  • Two overrides target the same rule. The later instrument normally supersedes, but this is a judgement. Fail loudly and let a person decide rather than picking by date silently.
  • A nonconforming structure passes a rule it should trigger. The status was modelled as a threshold substitution instead of an exemption from the existing condition, so new work is being excused too.
  • Verdicts change when the override store is refreshed. Expected — record the store’s version in the run manifest so the change is attributable rather than mysterious.

Part of: Rule engine design for zoning and setback automation

Conclusion

Exceptions are the part of a zoning code that lives in a filing cabinet rather than in the ordinance, and a rule engine that ignores them is confidently wrong about exactly the parcels whose owners are most likely to appeal. Model the three kinds distinctly, keep them in their own store keyed by parcel, require an instrument for every one, apply them at evaluation rather than by editing the code, and let the verdict say which authority governed. Done that way, an override is a documented decision the pipeline honours — not a special case somebody hard-coded and nobody can find.