While reviewing linopy/model.py, several typing improvements stood out on the main Model.add_* entry points. One is a latent correctness bug in the overloads; the rest are consistency / precision fixes that take advantage of aliases that already live in linopy/types.py.
1. add_constraints overload bug (correctness)
Model.add_constraints has overloads for freeze: Literal[False] = ... and freeze: Literal[True] = ... (model.py:884–913), but the runtime default is freeze: bool | None = None (model.py:927). None is the sentinel that means "use self.freeze_constraints".
Consequences:
- A caller passing
freeze=None explicitly matches neither overload.
- The overloads claim the default is
Literal[False], which is wrong: when self.freeze_constraints=True and the caller omits freeze, mypy infers Constraint but the runtime returns CSRConstraint.
Fix: add a third overload for freeze: None = ... (or no-arg) returning ConstraintBase, and drop the misleading defaults from the two typed-bool overloads (use :, not = ...).
2. add_variables uses bespoke types where aliases exist (model.py:656)
lower: Any = -inf, upper: Any = inf → should be ConstantLike. This matches what as_dataarray actually accepts at model.py:774–775, and Any defeats type-checking on the most common entry point.
coords: Sequence[Sequence | pd.Index | DataArray] | Mapping | None → CoordsLike | None. The inline form is also slightly narrower — it omits DataArrayCoordinates / DatasetCoordinates.
mask: DataArray | ndarray | Series | None → MaskLike | None. The current inline form drops DataFrame even though as_dataarray accepts it (used at model.py:784).
3. add_constraints runtime signature (model.py:915)
coords: Sequence[Sequence | pd.Index | DataArray] | Mapping | None should be CoordsLike | None, matching the overloads and as_dataarray.
4. add_objective (model.py:1093)
sense: str = \"min\" → Literal[\"min\", \"max\"]. The setter at objective.py:216–217 raises on anything else, so the wider str is just hiding a runtime error from the type-checker.
expr includes Sequence[tuple[ConstantLike, VariableLike]] but the body at model.py:1122–1124 only handles Variable | LinearExpression | QuadraticExpression. Either the type is too wide or the body is missing a branch — worth verifying whether the tuple-sequence path is reachable or dead.
Lower-priority
add_constraints lhs: ... | Callable — Callable is unparameterized. Tightening to Callable[..., ConstraintLike | AnonymousScalarConstraint] would document the rule contract but may break existing callers that return raw expressions/tuples.
- Consider introducing a
BoundLike = ConstantLike alias for symmetry — small, but it documents intent at add_variables better than reusing ConstantLike.
Suggested order
- Fix
add_constraints freeze overloads (correctness, not just hygiene).
- Tighten
add_objective.sense to Literal[\"min\", \"max\"].
- Replace inline
coords / mask / bound types with CoordsLike / MaskLike / ConstantLike across the three functions.
- Audit whether
Sequence[tuple[ConstantLike, VariableLike]] in add_objective is reachable or dead.
While reviewing
linopy/model.py, several typing improvements stood out on the mainModel.add_*entry points. One is a latent correctness bug in the overloads; the rest are consistency / precision fixes that take advantage of aliases that already live inlinopy/types.py.1.
add_constraintsoverload bug (correctness)Model.add_constraintshas overloads forfreeze: Literal[False] = ...andfreeze: Literal[True] = ...(model.py:884–913), but the runtime default isfreeze: bool | None = None(model.py:927).Noneis the sentinel that means "useself.freeze_constraints".Consequences:
freeze=Noneexplicitly matches neither overload.Literal[False], which is wrong: whenself.freeze_constraints=Trueand the caller omitsfreeze, mypy infersConstraintbut the runtime returnsCSRConstraint.Fix: add a third overload for
freeze: None = ...(or no-arg) returningConstraintBase, and drop the misleading defaults from the two typed-booloverloads (use:, not= ...).2.
add_variablesuses bespoke types where aliases exist (model.py:656)lower: Any = -inf,upper: Any = inf→ should beConstantLike. This matches whatas_dataarrayactually accepts at model.py:774–775, andAnydefeats type-checking on the most common entry point.coords: Sequence[Sequence | pd.Index | DataArray] | Mapping | None→CoordsLike | None. The inline form is also slightly narrower — it omitsDataArrayCoordinates/DatasetCoordinates.mask: DataArray | ndarray | Series | None→MaskLike | None. The current inline form dropsDataFrameeven thoughas_dataarrayaccepts it (used at model.py:784).3.
add_constraintsruntime signature (model.py:915)coords: Sequence[Sequence | pd.Index | DataArray] | Mapping | Noneshould beCoordsLike | None, matching the overloads andas_dataarray.4.
add_objective(model.py:1093)sense: str = \"min\"→Literal[\"min\", \"max\"]. The setter atobjective.py:216–217raises on anything else, so the widerstris just hiding a runtime error from the type-checker.exprincludesSequence[tuple[ConstantLike, VariableLike]]but the body at model.py:1122–1124 only handlesVariable | LinearExpression | QuadraticExpression. Either the type is too wide or the body is missing a branch — worth verifying whether the tuple-sequence path is reachable or dead.Lower-priority
add_constraintslhs: ... | Callable—Callableis unparameterized. Tightening toCallable[..., ConstraintLike | AnonymousScalarConstraint]would document the rule contract but may break existing callers that return raw expressions/tuples.BoundLike = ConstantLikealias for symmetry — small, but it documents intent atadd_variablesbetter than reusingConstantLike.Suggested order
add_constraintsfreezeoverloads (correctness, not just hygiene).add_objective.sensetoLiteral[\"min\", \"max\"].coords/mask/ bound types withCoordsLike/MaskLike/ConstantLikeacross the three functions.Sequence[tuple[ConstantLike, VariableLike]]inadd_objectiveis reachable or dead.