Missing values¶
Data arrives with gaps. A price feed skips a tick, a form leaves a field
empty, a join finds no match. Python code meets those gaps as None and
nan, and it usually treats them in one of a few ways without anyone
deciding it should. This page shows how mathema reads a value that is
not there, what a function can do with one, and how you write down what
yours does, so the claims about the numbers a function computes and the
claims about its gaps stay apart. Every output on the page is what the
code beside it prints.
Two kinds of no value¶
Where the None sits decides what it means.
price = None absent: the argument itself is not there
prices = [0.2, None, 0.7] a list with three slots:
slot 0 value 0.2
slot 1 hole, member null
slot 2 value 0.7
prices = [0.2, nan, 0.7] slot 1 is a hole, member nan
price = nan a hole: a float's own missing value
- absent: the object is not there. An argument, a field of a record,
a whole list. Python spells it
None. - missing: a slot holds no computable value. A
nanin a float, aNoneelement of a list, apd.NAorNaTin a pandas Series, a polarsnull. A missing value is called a hole, and each spelling is a member of the hole class:nan,null,NA,NaT.
A scalar argument has no slot inside it, so a scalar None is always
absent, while nan is always a hole. Here is what Python does with each:
import math
def volatility(variance: float) -> float:
"""The standard deviation for a variance."""
return math.sqrt(variance)
def total_exposure(positions: list) -> float:
"""The sum of every position."""
return sum(positions)
from prices import total_exposure, volatility
def outcome(expr):
try:
return repr(eval(expr))
except Exception as exc:
return type(exc).__name__
for expr in ['volatility(None)', 'volatility(float("nan"))',
'total_exposure([0.2, None, 0.7])', 'total_exposure([0.2, float("nan")])',
'total_exposure(None)']:
print(f"{expr:36} {outcome(expr)}")
volatility(None) TypeError
volatility(float("nan")) nan
total_exposure([0.2, None, 0.7]) TypeError
total_exposure([0.2, float("nan")]) nan
total_exposure(None) TypeError
The first and last calls pass None where an object belongs: absent. The
second passes a hole to a scalar, the third a list with a null hole in
slot 1, the fourth a list with a nan hole.
A claim states what its domain admits, completed from the annotation. A
float may be nan, so a claim over one admits it and mathema calls the
function there; an Optional[float] may also be absent; an int has no
hole. You write the short form, and the record shows what it resolved
to:
from typing import Optional
def volatility(variance: float) -> float:
return variance ** 0.5
def fee(amount: Optional[float]) -> float:
return 0.01 * amount
def lot_count(shares: int) -> int:
return shares // 100
def exposure(positions: list) -> float:
return sum(positions)
import mathema
from admits import exposure, fee, lot_count, volatility
def statement(fn, text):
rec = mathema.check(fn, claims=[mathema.claim(text, name="c")])
return next(p.statement for p in rec.probes if p.name == "c")
print(statement(volatility, "for variance in [0, 1], f(variance) >= 0"))
print(statement(fee, "for amount in [0, 100], f(amount) <= 1"))
print(statement(lot_count, "for shares in [0, 1000] subset Z, f(shares) >= 0"))
print(statement(volatility, "for variance in [0, 1] \\ {missing}, f(variance) >= 0"))
print(statement(exposure, "for positions in [0, 1]^n, f(positions) >= 0"))
for variance in [0.0, 1.0] : float|missing, f(variance) >= 0
for amount in [0.0, 100.0] : float|absent|missing, f(amount) <= 1
for shares in [0, 1000] : int, f(shares) >= 0
for variance in [0.0, 1.0] : float, f(variance) >= 0
for positions in ([0.0, 1.0] | {missing})^n : float, f(positions) >= 0
: float|missing says the claim admits a float's hole; \ {missing}
removes it, and the rendering keeps the exclusion because it says
something the type does not. Inside the brackets of (... | {missing})^n
the clause is about the slots of the vector.
Five behaviours¶
Given a hole or an absence, a function does one of five things. Each is defined by counting the missing and absent values in and out, so running the function decides which:
| word | the output holds |
|---|---|
raises |
nothing: the call raises |
drops |
no missing or absent value |
propagates |
as many as the input, of the same kind |
converts |
as many, of the other kind (a hole in, None out) |
introduces |
any other count, including one from inputs that were all there |
The ones that cause trouble are the quiet ones. Each of these returns an answer as if nothing had happened:
from typing import Optional
def clamp_discount(rate: float) -> float:
"""A discount rate held to [0, 1]."""
return max(0.0, min(1.0, rate))
def risk_label(score: Optional[float]) -> str:
""""high" above one half, else "low"."""
return "high" if score > 0.5 else "low"
def fee_rate(tier: int) -> Optional[float]:
"""The fee for a customer tier, None for a tier with no fee schedule."""
return {1: 0.05, 2: 0.07}.get(tier)
print(clamp_discount(float("nan"))) # min(1.0, nan) is 1.0
print(risk_label(float("nan"))) # nan > 0.5 is False
print(fee_rate(3)) # dict.get: None for a tier that exists
1.0
low
None
clamp_discount turns a missing rate into a full discount, and
risk_label turns a missing score into "low": both drop the hole
by an accident of comparison. fee_rate introduces an absence from
an input that was there.
A value claim is judged on values¶
A claim such as "clamp_discount(rate) stays in [0, 1]" is about the
numbers the function computes. It is judged wherever the function
returns a value, including the value it returns for a dropped hole, and
never where the function returns no value or raises at a missing input.
That call is sorted into one of the five behaviours instead, and the
record says what happened there. A nan from inputs that were all there
still fails a value claim: the mathematics has a value at that point,
and the code gave none.
One function at a time¶
The functions below come from one file of pricing code:
import math
from typing import Optional
def volatility(variance: float) -> float:
"""The standard deviation for a variance."""
return math.sqrt(variance)
def risk_label(score: Optional[float]) -> str:
""""high" above one half, else "low"."""
return "high" if score > 0.5 else "low"
def clamp_discount(rate: float) -> float:
"""A discount rate held to [0, 1]."""
return max(0.0, min(1.0, rate))
def log_return(ratio: float) -> float:
"""The log of a price ratio; a missing ratio is refused."""
if ratio != ratio:
raise ValueError("ratio is missing")
return math.log(ratio)
def in_base_currency(amount: float, fx_rate: Optional[float] = None) -> float:
"""An amount converted at fx_rate; no rate means it is already in base."""
if fx_rate is None:
fx_rate = 1.0
return amount * fx_rate
def fee_rate(tier: int) -> Optional[float]:
"""The fee for a customer tier, None for a tier with no fee schedule."""
return {1: 0.05, 2: 0.07}.get(tier)
The silent drop¶
Start with the failure. The claim is that a clamped rate lies in
[0, 1]:
import mathema
from pricing import clamp_discount
print(mathema.check(clamp_discount, claims=[mathema.claim(
"for rate in R, 0 <= clamp_discount(rate) <= 1", name="in_unit")]))
mathema.Record(clamp_discount) · source, no side effects · form bc9fa73b5bd1
in_unit for rate in R|missing, 0 <= clamp_discount(rate) <= 1 falsified at rate = nan
proven mathematics for rate in R, 0 <= clamp_discount(rate) <= 1
holds computation for rate in R, 0 <= clamp_discount(rate) <= 1 43 draws
falsified policy f(nan) no missing policy stated; returns 1.0
possible fixes:
(i) if dropping nan is intended, run: mathema accept pricing.clamp_discount missing[rate] --as discovery --corrected "missing(f, rate) drops"
(ii) exclude nan
(iii) handle nan at entry
Read it from the top. The headline is the claim as mathema resolved
it, rate in R|missing (a float may be nan), and its verdict. Under
it, the mathematics line is the claim over the real numbers, proven;
the computation line runs the real code in float64, and holds. The
last line is a policy line: what clamp_discount does with a
missing rate. No policy was stated, so mathema assumed the default for
a float, that a hole comes back as a hole, and the code does something
else: at rate = nan it returned 1.0. That falsifies the headline,
and the line under it lists the ways forward. If a full discount is what
a missing rate should mean, state it: missing(f, rate) drops. If not,
take nan out of the claim (for rate in R \ {missing}), or handle it
at entry, giving the hole back (if rate != rate: return rate) or
raising.
Two bugs in one line¶
from pricing import risk_label
print(mathema.check(risk_label, claims=[mathema.claim(
'for score in [0, 1], risk_label(score) in {"high", "low"}', name="labels")]))
mathema.Record(risk_label) · source, no side effects · form ba84c5dc4cc6
labels for score in [0.0, 1.0] : float|absent|missing, risk_label(score) in {"high", "low"} falsified at score = nan
holds computation for score in [0.0, 1.0] : float, risk_label(score) in {"high", "low"} 34 draws
falsified policy f(nan) no missing policy stated; returns "low"
possible fixes:
(i) if dropping nan is intended, run: mathema accept pricing.risk_label missing[score] --as discovery --corrected "missing(f, score) drops"
(ii) exclude nan
(iii) handle nan at entry
falsified policy f(None) no absent policy stated; raises TypeError
possible fixes:
(i) if the raise is intended, run: mathema accept pricing.risk_label absent[score] --as discovery --corrected "absent(f, score) raises(TypeError)"
(ii) exclude None
(iii) handle None at entry
The mathematics holds: every label is "high" or "low". The two
policy lines carry what it does not say, and both falsify the headline.
A missing score becomes "low", the same silent drop as above. And
Optional[float] promises to take None, which the function cannot:
None > 0.5 raises. No claim says it may; if the raise is what you
meant, state absent(f, score) raises(TypeError).
The code already says it¶
A float that propagates through a library call, a guard, and None used
as a flag each settle their own policy line:
from pricing import in_base_currency, log_return, volatility
print(mathema.check(volatility, claims=[mathema.claim(
"for variance in [0, 1], volatility(variance) >= 0", name="nonneg")]))
print(mathema.check(log_return, claims=[mathema.claim(
"for ratio in [0.5, 2], log_return(ratio) <= 1", name="bounded")]))
print(mathema.check(in_base_currency, claims=[mathema.claim(
"for amount in [0, 100], fx_rate in [0.5, 2], "
"in_base_currency(amount, fx_rate) == amount * fx_rate", name="scales")]))
mathema.Record(volatility) · source, no side effects · form fafd8ee932cd
nonneg for variance in [0.0, 1.0] : float|missing, volatility(variance) >= 0 holds
proven mathematics for variance in [0.0, 1.0] ⊂ ℝ, volatility(variance) >= 0
holds computation for variance in [0.0, 1.0] : float, volatility(variance) >= 0 43 draws
proven policy f(nan) propagates, from math.sqrt's own policy row, which f calls
mathema.Record(log_return) · source, no side effects · form cdfe6bbcc84d
bounded for ratio in [0.5, 2.0] : float|missing, log_return(ratio) <= 1 holds
proven mathematics for ratio in [0.5, 2.0] ⊂ ℝ, log_return(ratio) <= 1
holds computation for ratio in [0.5, 2.0] : float, log_return(ratio) <= 1 42 draws
proven policy f(nan) raises(ValueError), from the guard on line 3
mathema.Record(in_base_currency) · source, no side effects · form c0f6dfdbe44d
scales for amount in [0.0, 100.0] : float|missing, fx_rate in [0.5, 2.0] : float|absent|missing, in_base_currency(amount, fx_rate) = amount*fx_rate holds
holds computation for amount in [0.0, 100.0] : float, fx_rate in [0.5, 2.0] : float, in_base_currency(amount, fx_rate) = amount*fx_rate 161 draws
holds policy f(amount=nan) no missing policy stated; assumed propagates
holds policy f(fx_rate=nan) no missing policy stated; assumed propagates
proven policy f(fx_rate=None) drops, from the guard on line 3
volatility makes one call, to math.sqrt, and math.sqrt has its
own policy row in the bundled compendium: the hole comes back. The
policy line comes from it, proven. log_return raises on a missing ratio behind a
guard, so the row comes from the guard. in_base_currency replaces an
absent fx_rate with 1.0, a drop, read from the same kind of guard.
An absence the return type declares¶
from pricing import fee_rate
print(mathema.check(fee_rate, claims=[mathema.claim(
"for tier in [0, 3] subset Z, fee_rate(tier) < 1", name="below_one")]))
mathema.Record(fee_rate) · source, no side effects · form c1cba0e35dda
below_one for tier in [0, 3] : int, fee_rate(tier) < 1 proven
proven mathematics for tier in [0, 3] ⊂ ℤ, fee_rate(tier) < 1
proven policy absent(f) introduces from the return type Optional[float]: f returned None at tier = 0 from present inputs; confirmed on the draws of below_one
-> Optional[float] declares that the result may be absent, so the
None at tier = 0 is recorded and not judged, and absent(f)
introduces comes from the return type. The same body annotated ->
float fails the claim at that point: a None from inputs that were all
there is no value.
Containers: lists, arrays and series¶
A container's slots can hold holes, and each runtime type has its own
members: a list slot may hold null (a None element) or nan, a
numpy array nan, a pandas Series nan, null or NA. Every claim
over a container first meets the awkward cases (a one-slot vector, an
all-hole one, a hole at each end), then random draws with holes in
them.
import numpy as np
def total_exposure(positions: list) -> float:
"""The sum of every position."""
return sum(positions)
def average_return(returns: np.ndarray) -> float:
"""The mean of a series of returns."""
return float(np.mean(returns))
import mathema
from portfolio import average_return, total_exposure
print(mathema.check(total_exposure, claims=[mathema.claim(
"for positions in [0, 1]^n, total_exposure(positions) >= 0", name="nonneg")]))
print(mathema.check(average_return, claims=[mathema.claim(
"for returns in [0, 1]^n, 0 <= average_return(returns) <= 1", name="unit")]))
mathema.Record(total_exposure) · source, no side effects · form dacf931fef1e
nonneg for positions in ([0.0, 1.0] | {missing})^n : float, total_exposure(positions) >= 0 falsified at positions = [null]
proven mathematics for positions in ([0.0, 1.0])^n ⊂ ℝ, total_exposure(positions) >= 0
holds computation for positions in ([0.0, 1.0])^n : float, total_exposure(positions) >= 0 208 entries across 44 draws, sizes (1, 1) to (8, 1)
falsified policy f([..., null, ...]) no missing policy stated; raises TypeError
possible fixes:
(i) if the raise is intended, run: mathema accept portfolio.total_exposure missing[positions, null] --as discovery --corrected "missing(f, positions, null) raises(TypeError)"
(ii) exclude null
(iii) handle null at entry
holds policy f([..., nan, ...]) no missing policy stated; assumed propagates
mathema.Record(average_return) · source, no side effects · form ce47d44bdab7
unit for returns in ([0.0, 1.0] | {missing})^n : float, 0 <= average_return(returns) <= 1 holds
holds computation for returns in ([0.0, 1.0])^n : float, 0 <= average_return(returns) <= 1 418 entries across 102 draws, sizes (1, 1) to (8, 1)
proven policy f([..., nan, ...]) propagates, from numpy.mean's own policy row, which f calls
sum treats the two members of a list slot differently: it raises on a
None element and gives a nan back. The record says so member by
member, and the claim to write names the member. np.mean gives the
hole back, and the row comes from numpy's own. A pandas mean is a
longer story, told in the mean of nothing.
Strings and records¶
A string has no hole, so a str parameter admits nothing missing; an
Optional[str] admits absence. A language binding (L[unicode], see
language domains) draws real strings, and | {None} adds
the absence. The claim below is about a greeting:
from typing import Optional
def greeting(nickname: Optional[str]) -> str:
"""A greeting for a customer's nickname."""
return "Hi " + nickname.strip()
import mathema
from names import greeting
print(mathema.check(greeting, claims=[mathema.claim(
"for nickname in L[unicode] | {None}, len(greeting(nickname)) >= 3",
name="long_enough")]))
mathema.Record(greeting) · source, no side effects · form 4ddaf64c7461
long_enough for nickname in L[unicode]|absent, len(greeting(nickname)) >= 3 falsified at nickname = None
holds computation for nickname in L[unicode], len(greeting(nickname)) >= 3 223 draws
falsified policy f(None) no absent policy stated; raises AttributeError
possible fixes:
(i) if the raise is intended, run: mathema accept names.greeting absent[nickname] --as discovery --corrected "absent(f, nickname) raises(AttributeError)"
(ii) exclude None
(iii) handle None at entry
The computation holds on every string. nickname = None was drawn
first, because the binding admits it, and the function raised there;
the f(None) policy line says no claim allows that raise, which is why
the headline is falsified.
A record's field has the same two kinds, and where the None sits
decides which. A field or key holding None is absent, and so is a key
that is not there, an index past the end, or a step below an absent
object; an element of a list holding None is a hole. Absence has two
members, as the hole class has several: null, the key is there
holding None, and unset, the key is left out. They differ where a
difference matters (in a PATCH body, {} leaves a note alone and
{"note": null} clears it), so a binding on a path can admit or
exclude each, \ {unset} or \ {null}:
def delivery_note(order: dict) -> str:
"""The note printed on a delivery slip."""
return order["note"].strip()
import mathema
from slips import delivery_note
print(mathema.check(delivery_note, claims=[mathema.claim(
'for order.note in {"ring twice", "leave at the door"} | {None} \\ {null}, '
'len(delivery_note(order)) >= 1', name="has_text")]))
mathema.Record(delivery_note) · source, no side effects · form 723add5de9a8
has_text for order.note in {"leave at the door", "ring twice", absent} \ {null}, len(delivery_note(order)) >= 1 falsified at order.note unset
holds computation for order.note in {"leave at the door", "ring twice", absent} \ {null}, len(delivery_note(order)) >= 1 87 draws
falsified policy f(order) at order.note unset no absent policy stated; raises KeyError
possible fixes:
(i) if the raise is intended, run: mathema accept slips.delivery_note absent[order.note] --as discovery --corrected "absent(f, order.note) raises(KeyError)"
(ii) exclude unset at order.note
(iii) handle unset at entry
A field's no-value is a missing input as a parameter's is: the value
claim is judged on the notes that are there, and the raise at the key
left out is the path's policy line under the claim, which is why the
headline is falsified. absent(f, order.note, unset) raises(KeyError) states it; the
second argument of a policy row is a parameter or a path, with the same
member forms (absent(f, order.note, null) drops,
missing(f, order.lines[*].qty) propagates).
A witness names the member: order.note unset for a key left out,
order.note = null (absent) for a key holding None, and
order.lines[1] = null (hole) for an element of a list. On a path that
ends at a field null is the absence member; on one that ends at an
element (order.lines[*]) it is the hole member. Written for a
parameter itself, null is its absence, the same as None.
is_absent_safe(f) reaches into a record's fields: an Optional field
of a dataclass or a pydantic model is called with None too.
Policy rows, defaults and mathema claims --write¶
A policy row is a claim like any other, in one short form:
missing(f, rate) propagates
absent(f, score) raises(TypeError)
missing(f, positions, null) raises(TypeError)
assuming count(xs) >= 1, missing(f, xs) drops
absent(f) introduces
The member narrows a row to one spelling; a premise on count(...)
(the number of value slots) or len(...) (every slot) splits a
behaviour where the size decides it.
A row decided from calls carries the route probe:counterfactual: each
call at a hole is made again with the hole filled, and the difference
says what the function did with it. clamp_discount returns 1.0 for a
missing rate; filled with 1.0 and with 2.0 it returns 1.0 again,
but filled with 0.9 it returns 0.9, so it read the slot and drops
the hole. When no fill changes the answer, the function is
indifferent to the slot: it returned a value with the hole there, which
breaks raises and propagates, and says nothing for drops,
where ignoring a slot and replacing a hole look the same. Where a kind reaches a parameter,
the record carries a row for it, and the bracket says whose word it is:
- default for a float, which may be nan: the type admits the kind
and nobody said anything. A hole propagates by default; a
Nonean unannotated parameter may take raises. - from math.sqrt's own policy row or from the guard on line 2: the code says it.
- observed: you admitted the kind (an
Optional, a listedNone), so there is no default; what the code did is written down, and a raise stays open until a claim says it may. - stated: yours.
A row the code contradicts, and a raise no claim accounts for, fail
mathema verify like any falsified claim. mathema claims lists a
function's rows by state, and --write puts them in your claims file,
where changing a policy is editing one word:
import math
def volatility(variance: float) -> float:
"""Claims:
nonneg: for variance in [0, 4], f(variance) >= 0
"""
return math.sqrt(variance)
def clamp_discount(rate: float) -> float:
"""Claims:
in_unit: for rate in R, 0 <= f(rate) <= 1
"""
return max(0.0, min(1.0, rate))
$ mathema claims shop.clamp_discount
shop.clamp_discount: no declared claims (to list candidates, run: mathema claims shop.clamp_discount --suggest)
shop.clamp_discount: 1 policy row about rate
contradicted by the code (change the word, the code, or accept it as a discovery; --write writes these with the contradiction in the note):
falsified missing[rate]: missing(f, rate) propagates [mathema's default word for a float, not a claim of yours; f drops instead: nan in, 1.0 out]
(i) if 1.0 is the answer f should give for a missing rate, state: missing(f, rate) drops
(ii) if not, make f raise or give nan back
(iii) to accept it as a discovery, run: mathema accept shop.clamp_discount missing[rate] --as discovery --corrected "missing(f, rate) drops"
$ mathema claims shop.clamp_discount --write
shop.clamp_discount: wrote 1 policy row to claims/policies.claims.yaml: missing[rate].
The code contradicts missing[rate] (f drops, nan in, 1.0 out):
(i) change the word in the file or change f
(ii) to accept it as a discovery, run: mathema accept shop.clamp_discount missing[rate] --as discovery --corrected "missing(f, rate) drops"
$ cat claims/policies.claims.yaml
shop.clamp_discount:
claims:
- name: missing[rate]
statement: missing(f, rate) propagates
note: 'written by mathema claims --write: mathema''s default word for a float,
not a claim of yours; contradicted by the code on 2026-09-30: f drops, nan in,
1.0 out'
The contradicted row is written with the contradiction in its note, so
the next mathema verify fails on it until you change the word, change
the code, or accept it as a discovery.
Which values are holes¶
Which values a runtime holds as missing is a definition, stated once
under the runtime type, missing := {null, nan}, and taken at face
value. mathema ships the ones for pandas and polars; a project adds its
own the same way (see definitions). A
claim over a polars Series resolves missing to that runtime's members,
and the record says so:
import polars as pl
def total_volume(volumes: pl.Series) -> float:
"""The traded volume over a session."""
return float(volumes.sum())
import mathema
from pl_prices import total_volume
rec = mathema.check(total_volume, claims=[mathema.claim(
"for volumes in [0, 1]^n, total_volume(volumes) >= 0", name="nonneg")])
(row,) = [p for p in rec.probes if p.name == "nonneg"]
print(row.meta["mathema.missing"]["means"])
missing for volumes (polars.Series) means null or nan
The gates and enforce_domain¶
A policy row is one fact about one parameter. is_missing_safe(f) and
is_absent_safe(f) state that every parameter admitting the kind has a
policy the code follows at every member. mathema offers them
(mathema claims KEY --suggest) and never asserts them for you.
print(mathema.check(risk_label, claims=[mathema.claim(
'for score in [0, 1], risk_label(score) in {"high", "low"}'),
mathema.claim("is_missing_safe(f)"), mathema.claim("is_absent_safe(f)")]))
mathema.Record(risk_label) · source, no side effects · form ba84c5dc4cc6
risk_label_score_in_high_low for score in [0.0, 1.0] : float|absent|missing, risk_label(score) in {"high", "low"} falsified at score = nan
holds computation for score in [0.0, 1.0] : float, risk_label(score) in {"high", "low"} 34 draws
falsified policy f(nan) no missing policy stated; returns "low"
possible fixes:
(i) if dropping nan is intended, run: mathema accept pricing.risk_label missing[score] --as discovery --corrected "missing(f, score) drops"
(ii) exclude nan
(iii) handle nan at entry
falsified policy f(None) no absent policy stated; raises TypeError
possible fixes:
(i) if the raise is intended, run: mathema accept pricing.risk_label absent[score] --as discovery --corrected "absent(f, score) raises(TypeError)"
(ii) exclude None
(iii) handle None at entry
holds is_missing_safe[f]: is_missing_safe(f)
score (float): nan drops, confirmed by calling f at score = nan; no claim states it yet
falsified is_absent_safe[f]: is_absent_safe(f)
score (float): None raises TypeError, and no claim says it may
counterexample score = None: f raised TypeError
score is Optional[float], so f promised to take None.
(i) if the raise is intended, state: absent(f, score) raises(TypeError)
(ii) if not, handle None in f, or annotate score as float
(iii) to accept the raise as a discovery, run: mathema accept pricing.risk_label absent[score] --as discovery --corrected "absent(f, score) raises(TypeError)"
is_missing_safe holds, not proven: the one hole a float holds was
tried and f drops it, but that is what the code did, not what anyone
said it should do. Writing missing(f, score) drops makes it a stated
policy, and the gate proven. is_absent_safe is falsified by the raise
on None, with the claim to state beneath it.
@enforce_domain() turns the policy rows into a runtime check. It is
opt-in. A raises row refuses the input before the function runs, with
the exception the row names (mathema's DomainError where it names
none) and one sentence naming the parameter and the member; a drops or
propagates row is checked on the result, with a MissingValueError (a
kind of DomainError); converts and introduces enforce nothing:
import math
from typing import Optional
import mathema
@mathema.enforce_domain()
@mathema.claims_decorator("absent(f, variance) raises(TypeError)",
"missing(f, variance) propagates")
def volatility(variance: Optional[float]) -> float:
return math.sqrt(variance) if variance == variance else 0.0
for arg in (None, float("nan"), 4.0):
try:
print(volatility(arg))
except (TypeError, mathema.DomainError) as exc:
print(exc)
enforce_domain is active and raised TypeError because variance is None
volatility(): at variance = nan f returned 0.0, dropping the hole, but its policy says propagates (missing(f, variance) propagates)
2.0
What is not missing¶
- An empty container: zero slots, so no holes.
is_empty_safe(xs)asks what the function does with one, building it as the parameter's own runtime type. - An infinity: a value. An infinity from a finite input fails a value
claim, as a
nanfrom inputs that were all there does. - A row a function filters out of a table: a change of shape, not a hole.
What changed in 0.6.1¶
- The two words,
absentandmissing(Nonestill reads asabsent), with the membersnan,null,NAandNaT; a domain renders what it admits. - A value claim is judged on values; a missing input is sorted into one of the five behaviours.
- Policy rows, their defaults,
mathema claims --write, and the bundled policy rows formath, numpy, pandas and polars. is_missing_safe(f),is_absent_safe(f),is_empty_safebuilt through the runtime type, andenforce_domainreading the policy rows.