Skip to content

[MAINTENANCE] Declare metric values, render-content payloads, and metric keys as what they actually are - #12116

Open
joshua-stauffer wants to merge 5 commits into
developfrom
m/typing-shared-seams
Open

[MAINTENANCE] Declare metric values, render-content payloads, and metric keys as what they actually are#12116
joshua-stauffer wants to merge 5 commits into
developfrom
m/typing-shared-seams

Conversation

@joshua-stauffer

@joshua-stauffer joshua-stauffer commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Tightens four narrow but widely-repeated type declarations to what they actually admit, without changing any runtime behavior:

  • MetricValue (a resolved metric value, in validator/computed_metric.py) is now spelled as the dynamic type it always effectively was, instead of a closed Union that every call site had to re-narrow before doing anything with it.
  • The render-content constructors in render/components.py now declare the parameter shapes their callers legitimately pass (widened, never narrowed).
  • 168 call sites across 52 files that built render-content objects by splatting a literal dict now pass the same arguments directly as keywords, so each argument is checked on its own rather than being erased into one dict type.
  • A handful of test call sites that built metric-cache keys as bare tuples now build them with the named identifier type the metric interfaces already declare.
  • The two base renderer declarations now document the contract the dispatcher actually offers: it invokes renderers with different argument sets on different branches, which is why both parameters are optional. No renderer body changes in this PR — see below.

Why

A union wide enough to accept every assignment and permissive enough to satisfy every read checks nothing — it just multiplies the number of places a real defect could hide. Once metric values and render-content payloads are described accurately, the type checker can tell the difference between a legitimate call and a genuine mistake, instead of forcing every caller to defeat the checker with a local suppression.

None of these are behavior changes: every widened parameter accepts a strict superset of what it accepted before, every changed key construction produces the same runtime value as the bare tuple it replaces (the named identifier type is a NamedTuple, so it compares and hashes equal), no renderer signature changed, and no renderer body changed behaviorally (many of the call-site rewrites above sit inside renderer methods, but each is the same call written differently).

Known residue, deliberately not fixed here

Writing the dispatch contract down made it clear that a number of renderers read configuration or result unconditionally, even though the dispatcher has reachable branches that omit either one. Those methods raise when they hit such a branch. These are pre-existing defects, not introduced here, and they are left alone deliberately: adding a guard would change what happens on those paths, which is a behavior change and does not belong in a typing PR. They are tracked for separate follow-up, along with one render view whose signature is narrower than its own body and docstring allow.

Because those defects are unfixed, this PR does not drive the renderer-nullability diagnostics to zero. That residue is expected and accounted for.

User impact

None beyond fewer false type-checker diagnostics for anyone type-checking code that calls into these APIs. No public name moved, no parameter was removed or renamed, no already-declared parameter type was narrowed, and no renderer's declared signature changed in any way — parameter names, order, and defaults are byte-identical before and after, verified by an automated before/after signature comparison across every render-content class the render package exports and every renderer method this change touches.

How to review

  • great_expectations/validator/computed_metric.py — the MetricValue redefinition and its compatibility rationale in the comment above it, including what changes at runtime (introspecting the alias's members now returns none) and why nothing in this repository does that.
  • great_expectations/render/components.py — the four widened constructor parameters (table, header, header_row, collapse_toggle_link); confirm each widening is a strict superset of the prior type.
  • The call-site changes (splat → direct keyword arguments) are mechanical and account for most of the diff. Reviewer effort is best spent spot-checking a few for argument-order and value fidelity rather than reading all 168.
  • great_expectations/expectations/expectation.py — the two added docstrings on the base renderer declarations. These are documentation only; the commit adds 24 lines and deletes none.
  • Suppression comments are removed, never added: 20 # type: ignore comments became unused once the declarations were corrected, and the type-check configuration rejects unused ignores.

MetricValue was a Union whose first member was Any, the carrier for
deferred SQL/Spark expression objects. A union containing Any accepts
every value on assignment, so it added no checking power there. What it
did add was cost: each read of a MetricValue-typed attribute was checked
against every other member in turn, so one read produced one diagnostic
per member whenever the static type could not be proven to satisfy them
all -- multiplying a single signal into many duplicate, non-actionable
ones.

Spelling the alias as Any keeps every existing annotation and assignment
valid in both directions while collapsing that multiplication at its
source, instead of leaving each caller to suppress it individually. The
former member inventory is preserved as documentation at the definition
site, along with the runtime consequence: the name, import path, and
re-export are unchanged, but the object bound to the name is no longer a
Union, so introspecting its members now returns none. No code in this
repository does that, or uses the alias as a validated model field.

One suppression in the fluent integration test utilities silenced a
diagnostic this union produced; with the union gone the comment is dead,
and the type-check configuration rejects unused ignores, so it goes too.
These tests build metric mappings by hand and key them with bare
three-element tuples, while the interfaces they hand them to declare
MetricConfigurationID. A mapping declared to use a named key type should
be keyed with it: the reader learns what the key means, and the checker
can tell a metric identifier from any other string triple.

The substitution is behavior-preserving. MetricConfigurationID is a
NamedTuple, so it is a tuple: it compares equal to, and hashes equal to,
the bare tuple it replaces, and a lookup written either way finds the
same entry. Only key expressions and the key slots of existing
annotations change here -- no expected value, no assertion, and no test
logic. Where a configuration object was already at hand, its own id
property is used rather than respelling the tuple.
These call sites built a literal dict and splatted it into a render
content constructor. A splatted mapping cannot be checked argument by
argument: the checker compares the dict's joined value type against
every candidate parameter, so one call produces a diagnostic per
parameter the joined type does not satisfy -- many duplicates standing
in for at most a few real mismatches, and no indication which argument
is actually wrong.

Written as direct keyword arguments, each value is checked against its
own parameter. The rewrite is mechanical and behavior-preserving: same
arguments in the same order, same value expressions, same evaluation
order, the same TypeError for an unknown keyword, and the same object
built. The render suites pass unchanged.

Where normalization resolved a mismatch, the suppression that had
covered the whole dict is now unused and is dropped; where a genuine
per-argument mismatch remains, that same suppression moves to the one
argument it applies to, so it no longer hides its neighbours.
With the splat call sites written as keyword arguments, the checker can
finally evaluate each argument against its own parameter, and a handful
of genuine mismatches surfaced underneath the noise. Each one is a case
of the declaration being narrower than what callers legitimately pass,
so each is fixed at the declaration.

Every change here widens: a table's rows and a table's header row carry
several shapes across the renderers that build them, a header may be
plain text as well as rendered content, and so may a collapse toggle
link. Nothing is narrowed, no parameter is added, removed or renamed,
and no default changes -- these signatures are a public override point,
and code that type-checked against them before still does.

Three suppressions in the expectation renderers covered mismatches these
declarations now accept; with the diagnostics gone the comments are dead
and the type-check configuration rejects unused ignores, so they go too.
Two renderers annotate a local they build incrementally, which the
checker could not otherwise infer. The docstrings are corrected where
they described the old, narrower promise.
The dispatch invokes prescriptive and diagnostic renderers with
different argument sets on different branches: one path passes a
configuration and omits the result, another passes a validation result
and omits the configuration, and a third passes a configuration taken
from a validation result that may not carry one. That is why both
parameters are declared optional -- the signature is the dispatch
contract, and a renderer that assumes either is present is reading a
guarantee the dispatch does not make.

This states that contract once, on the base declarations renderers
override, so an author writing a new one can see it. The signatures
themselves are unchanged: they are a public override point, and this
commit adds no parameter, removes none, and changes no default or
declared type.

Existing renderers that read these parameters unconditionally are left
as they are. Guarding them here would change what happens on the paths
where the argument really is absent, which is a behavior change rather
than a typing one, so those are reported separately for their own fix.
@netlify

netlify Bot commented Aug 28, 2026

Copy link
Copy Markdown

Deploy Preview for niobium-lead-7998 canceled.

Name Link
🔨 Latest commit 22d12d0
🔍 Latest deploy log https://app.netlify.com/projects/niobium-lead-7998/deploys/6a90d4d0eb00b700089a6cc0

@joshua-stauffer
joshua-stauffer marked this pull request as ready for review August 28, 2026 00:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant