Errors, warnings, and notes
What the library says when something is wrong, doubtful, or guessed, and the few errors and warnings that are classes of their own.
Justified by best-practices/principles.md.
The policy
Prefer guessing a default and printing it to raising an error. Most things a user leaves out have a sensible default, and stopping the run to ask for it helps nobody. An error is for the cases where there is no sensible guess: a name that does not exist, lengths that do not match, a request the solver cannot meet.
A message is read once, by an epidemiologist or by an AI writing a model for one. Either way it should say what is wrong in the user’s own terms, name the thing it is about and what exists instead, and say what to do.
Message shape
Every error SHOULD have:
- The right built-in type: a
KeyError(raised assc.KeyNotFoundError, so the message is not quoted) for a name that is not found, aTypeErrorfor a value of the wrong kind, anAttributeErrorfor something that cannot be read, and aValueErrorfor the rest. A class of its own only where code needs to catch that kind of error apart from others (see Errors and warnings that are classes). - One line saying what is wrong, using the names the user gave things.
- What to do about it, as code the user could paste where possible, with the closest matching names for anything misspelled (and none when nothing is close).
The message is what is searched for, documented, and tested (pytest.raises(KeyError, match='has no parameter called')), so it is kept word for word across versions where it can be.
TypeError: the rate of the transition "recovery" is a plain number (a rate times a duration), not a rate:
rate = peryear(0.1) * days(3) = 0.00082158
A rate times a duration is a probability over that duration, not a rate; give the rate itself, e.g. rate=ss.peryear(0.1).
Avoid a bare ValueError: shape mismatch or KeyError: 'beta', and a traceback through library internals as the main message. Notes are one line each and need only say what was assumed or done.
Levels
| Level | Means | What happens |
|---|---|---|
| Error (a built-in exception, or one of the classes below) | There is no sensible guess, or the request cannot be met | Raises an exception |
Warning (a RuntimeWarning, or one of the classes below) |
The run can proceed, but something probably needs attention | Issued through Starsim’s ss.warn, so Python’s warnings filters and ss.options.warnings='error' apply; the run continues |
Note (ss.note()) |
Something was assumed, approximated, or is worth knowing | Kept in sim.notes whatever the verbosity; printed as one line when verbose is 1 or more |
verbose is one integer for every solver: 0 prints nothing, 1 (the default) the solver chosen, the notes, and the progress of a run, and 2 everything, including each step.
Most guessed defaults are not named at all: describe() marks them [guessed] (01-objects.md). The few that also print a line when the sim is built (the unit, a state treated as a count) are notes. Approximations made when a solver is requested explicitly are unnamed one-line notes (12-backends.md); approximations='allow' keeps them without printing them, and they turn into CapabilityError under approximations='error'. The one approximation solver='auto' makes without being asked, Erlang stages for a dwell time, is a note too.
Errors and warnings that are classes
Each is ss.errors.<Name>, and subclasses its built-in directly. An error is a class when code catches it by kind, or when it owns its message (builds it from what it is given, raised from more than one place, or part of an API contract); a warning is a class when it is worth filtering by kind (silenced in sweeps, or escalated to an error in a pipeline).
| Name | Kind | Why it is a class |
|---|---|---|
CapabilityError (also ss.CapabilityError) |
error (a ValueError) |
The requested solver cannot run part of the model. Caught by solver='auto' to run as agents instead, and by users trying another solver. Names the part, what it asked for, and what would fix it |
AlreadyRunError (also ss.AlreadyRunError) |
error (a RuntimeError) |
A sim or module that has already run (or been finalized) is stepped, finalized, or added to again. Adds “To run it again from the start, call sim.run()” to every message, from the sim and from a module |
DistNotInitializedError |
error (a RuntimeError) |
A distribution made with strict=True is sampled before it is initialized. Builds its message from the distribution; the error of common random numbers that a model author meets |
DistSeedRepeatError |
error (a RuntimeError) |
Two distributions have the same seed, or a distribution is jumped back to a state it has passed. Builds its message from the two distributions |
BooleanOperationError |
error (a NotImplementedError) |
A logical operation on an array of people’s values that is not Boolean (ppl.age & ppl.female). Builds its message from the array’s type, for the four operators |
AtBound |
warning | A best-fit value is at or near one of its bounds. Silenced by the fit itself while a profile is computed, and by users in sweeps |
NotConverged |
warning | The chains of a posterior disagree, or draws diverged. A user may escalate it to an error in a pipeline |
LossyExport |
warning | An export to another format, or to code, left something out or changed it. The same: an export that silently loses things matters |
ImportIncomplete |
warning | An import could not translate something. The same, for imports |
Every other error is a built-in (see Message shape), every other warning a RuntimeWarning, and every note a line from ss.note().
Ordinary usage errors
Some mistakes are ordinary Python usage errors, and raise a TypeError or ValueError with a message in the same shape, as every other error does:
- giving both
rate=anddur=to a transition, or more than one ofmultiply=,add=,set=toss.modify; - a state’s name where the state is meant (
ss.Transition(I, 'R', dur=10)), which says to declare the states; - a set of sources without parentheses (
S | I | R >> D @ mu); - using a lazy condition where Python needs a single truth value (
if,and,or,not); the message says to use&,|,~; - reading a result from a module that was given to a sim (
sir.results.Iafterss.Sim(sir).run()): the sim ran its own copy, so theAttributeError(orKeyError) says where the result is (sim.results.sir.I).
Removed since the first draft
The first draft had about 75 codes and the second 54. Most of the ones that went were errors for mechanisms that no longer exist (queued updates, the closure ladder, declared reads and writes, the dict shorthand, predicate rank) or refusals that are now a guessed default or a printed approximation. In particular: a missing n_agents is guessed; a spatial dimension without mobility is an ordinary model; coverage and capacity on a stratified model are behaviors the library is required to get right (05), not errors; and running a network model as an ODE prints a note.
Since the second draft: E901 ResultExists went with result caching, which is deferred. W101 was UnitAssumed (“no unit given, assuming days”); a model with no unit now runs in timesteps and the code reports an inferred unit instead. An unmatched data column was a warning (W705) and is an error (E705), now that ss.SimData(aliases=, ignore_extra=) gives a way to deal with it. Seven codes were added, for states that are not people, sim-level stratification, units, and solver='auto': W103, E210, E211, W212, E308, W404, and W1205.
Since the review of October 2026: the numbers were dropped, and each error, warning, and note is a class named as before (E804 CapabilityError is ss.CapabilityError), and the catalog was made from the code. The codes that nothing raised went: W103 UnitsDisagree, W207 UnreachableState, W309 LargeStateSpace, W1002 CoarseFlow, W1203 LeapConditionMarginal, W1204 DiscretizationCoarse, E1003 RunAfterFinish (a finished sim raises ss.AlreadyRunError), and E1401 SchemaTooNew. Three that were raised but not listed were added: UnknownEffect, UnknownDisease, and UnknownLocation. OverlappingSet is a note, as the code has it, not a warning. Every note is kept in sim.notes, including under verbose=0 and approximations='allow', which only stop it being printed.
Since the review of over-engineering (27 October 2026): the named classes went, with ss.CodedError, ss.StarsimWarning, ss.Note, the category= of ss.note(), and the catalog. Each error is a built-in (a lookup a KeyError, typed arithmetic a TypeError, an unkept time an AttributeError, the rest a ValueError), with its message word for word; three warnings are plain RuntimeWarnings; and the ten notes are lines. The five errors and four warnings that code catches by kind are kept (above).
Documentation and tests
Every error, warning, and note SHOULD have a test that triggers it, keyed by a distinctive part of its message (tests/devtests/test_errors.py), and the tests that check for an error match its message (18-conformance.md).