Pyomo style guide#
This guide promotes consistent, readable, and maintainable Pyomo code for modeling and optimization. It complements standard Python style conventions, such as PEP 8, and focuses on modeling clarity, reproducibility, and separation of concerns, with specific recommendations for Pyomo. It is not intended as an alternative to the core Pyomo documentation.
Comments and suggestions are welcome!
Workflows#
Pyomo model development is most effective when data preparation, modeling logic, solver interaction, and analysis are clearly separated. The key principles are:
Data pre-processing is external to the model.
Clean and validate data before creating Pyomo components.Model-building functions improve modularity.
A function that takes data and returns aConcreteModel()ensures reproducibility and isolates side effects.Deployment differs from development.
Production code usually omits exploratory validation steps but still benefits from well-structured model-building logic.
By following these principles, Pyomo models become easier to test, document, and integrate into wider analytical pipelines.
This guide addresses a typical Pyomo workflow shown in this diagram:

In most use scenarios, there will be also additional steps like post-processing and analysis of solution data and validation of model results. These steps are not shown in the diagram for clarity.
Subsequent deployment of Pyomo model will omit the development and validation steps, but may integrate the remaining elements into existing application workflows. This style guide supports development and deployment workflows by emphasizing modularity and clean interfaces between successive stages in the standard Pyomo workflow.
This guide focuses on the workflow stage labeled “Python Model Builder” where the optimization model consists of Python code that accepts problem specific data to produce a Pyomo model object. Well-written Python code will use the Pyomo package to reflect the structure of the mathematical model on which it is based, and accept commonly used data sources to produce solvable model instances.
Coding Conventions#
The following conventions target clarity, maintainability, and consistency across Pyomo codebases. They supplement standard Python style guides and reflect common best practices used in research and production settings.
Use pyo as the preferred alias for Pyomo#
Name space conflicts in Python are a common and avoidable source of runtime errors. Pyomo, for example, overloads functions commonly encountered in mathematical applications, such as sin, log, exp. As a consequence, this sequence of imports
# discouraged, don't do this!
from pyomo.environ import *
from numpy import as *
will overwrite Pyomo’s math functions and lead to unexpected results and errors. For this reason, importing Pyomo directly into the Python name space is strongly discouraged.
Name space conflicts with commonly used packages are avoided by importing libraries with alias. Regrettably, the available documentation has included multiple aliases for Pyomo, among them are pe, pmo, and aml. Here we propose the consistent use of pyo as an alias for pyomo.environ.
# preferred, do this!
import pyomo.environ as pyo
If a more concise notation is absolutely required for presentations or classroom use, for those specific cases import only the necessary Pyomo objects. For example
# acceptable in teaching/demo contexts
from pyomo.environ import ConcreteModel, Var, Objective, maximize, SolverFactory
Use pyo.ConcreteModel instead of pyo.AbstractModel#
Optimization applications required a close coupling of mathematics and problem data. Data sources are needed to determine values for the parameters appearing the mathematical description of the problem. For algebraic modeling languages like Pyomo, this raises a “chicken or the egg” dilemma: Which comes first, the algebraic model represented in the modeling language, or the data that specify values for the model parameters?
Pyomo provides for both possibilities. A Pyomo pyo.AbstractModel object can be created before the data is known. Pyomo includes methods to load parameter values after the data becomes available. This establishes a two phase procedure where the second phase employs Pyomo methods to access data and create a specific model instance.
In contrast, a Pyomo pyo.ConcreteModel object is created from known data. This results in single phase of model building where components are fully constructed and initialized when they are first attached to the pyo.ConcreteModel object.
Here we encourage the consistent use of pyo.ConcreteModel to build Pyomo models. The main reason is that Python offers tools for wrangling data from a wide range of sources. The tools range from standard Python data structures like tuples, lists, and dictionaries, to feature-rich packages such as Pandas and datatable. These tools can used to construct and initialize parameters in a ConcreteModel as it is constructed. Using pyo.ConcreteModel removes the need to create and store intermediate forms of the algebraic model or, for that mater, to pre-process the model data for later use in an AbstractModel.
This is the preferred syntax for creating a Pyomo model
# do this to create a model instance
m = pyo.ConcreteModel('model description')
Use pyo.Set and pyo.RangeSet to index model components#
The ability to index components, such as variables and constraints, is essential to scaling a model for larger applications. In general, components attached to a Pyomo ConcreteModel can be indexed by ordered Python iterables such as lists, dictionary keys, tuples, and ordered sets. In principle, this enables a deep integration of Python data structures with Pyomo models.
In practice, however, the use of pyo.Set and pyo.RangeSet to create index sets is encouraged for the following reasons:
Pyomo
SetandRangeSetprovides a consistent and uniform a uniform interface between data pre-processing and model creation.Pyomo Set provides flexible options for filtering, validating, and manipulating sets.
Internally, Pyomo uses Sets and RangeSets to trace model dependencies to provide better error reporting and sensitivity calculations.
Pyomo creates an associated internal Pyomo Set each time a Python iterable is used to create indexed model objects. Creating of multiple indexed objects with the same iterable produces redundant sets.
To illustrate, consider a Python dictionary
bounds = {"a": 12, "b": 23, "c": 14}
this
# do this
m.B = pyo.Set(initialize=list(bounds.keys()))
m.x = pyo.Var(m.B)
is preferred to this.
# don't do this
m = pyo.ConcreteModel()
m.x = pyo.Var(bounds.keys())
pyo.Set() and pyo.RangeSet() incorporate a significant collection of methods to operate on sets operations. As general guidance, operations on sets should are preferred over complex looping and conditional logic on set components.
Parameters#
Pyomo parameters are instances of the pyo.Param() class. Parameters hold problem data, may be indexed, and are attached to specific model or block. When combined with sets, parameters and set objects can completely define and modularize the interface application data and the optimization model.
As a simple example, consider a parameter \(a_{i,j}\) for \(i\in I\), \(j\in J\), appearing in a mathematical model, where \(a_{i,j}\) refers to an element in a Pandas DataFrame named df. Using Pyomo’s decorator notation, this parameter can be initialized by
@m.Param(m.I, m.J)
def a(m, i, j):
return df.loc[i, j]
By default, Pyomo parameters are immutable which assures their values will be consistent throughout the model construction and transformations. Parameters determining the size of index sets, or fixed upper and lower bounds on decision variables, are examples where using an immutable Pyomo parameter is good practice.
Mutable parameters are created with a keyword option mutable=True in the decorator. Mutable parameters are useful for optimization models that will be solved multiple times as a function of a few parameters. The use of mutable parameters should be limited and intentional.
Variables#
Decision variables are attached to a specific model or block, may be indexed, and may be restricted to particular sets or range of values.
Use domain rather than within#
pyo.Var() accepts a keyword option within or domain to specify a class of decision variables. To reduce the cognitive burden for a new user or when reading Pyomo models, consistent use of domain is preferred because of its common use in mathematics to represent the set of all values for which a variable is defined.
Use bounds when known and fixed#
A Pyomo model can place bounds on decision variables with either the bounds keyword in the argument to pyo.Var, or as explicit constraints in the model.
When upper or lower bounds for a variable are known and fixed, best practice is to use of bounds when creating the variable. This practice can reduce the number of explicit constraints in the model and simplify coding and model display.
Constraints and Objective#
Prefer pyo.Constraint to pyo.ConstraintList#
The pyo.ConstraintList() class is useful for creating a collection of constraints for which there is no simple indexing, such as implementing algorithms that employ constraint generation. pyo.Constraint(), however, should always be preferred when the constraints can be indexed over known sets.
Use decorators to improve readability#
Pyomo supports both explicit component declarations with rule= and Python’s
decorator notation. Both can construct the same optimization model. Decorators
keep the rule beside its declaration and avoid repeating a component’s name.
They do not change the mathematical problem or the algorithm used by a solver.
First: what does Python’s @ mean?#
A decorator is a callable applied to the function defined immediately below it. In ordinary Python, these two patterns have the same effect:
@decorate
def rule(x):
return x + 1
def rule(x):
return x + 1
rule = decorate(rule)
Here decorate is a placeholder for a previously defined callable. With
@factory(options), Python first evaluates factory(options) to obtain the
decorator, then applies it to the new function. A decorator need not wrap or
change the function: it can register it elsewhere and return it unchanged.
That is the pattern used by Pyomo’s model-component decorators.
A complete indexed-constraint example#
Run this setup before either of the two alternatives below:
import pyomo.environ as pyo
model = pyo.ConcreteModel()
model.S = pyo.Set(initialize=["A", "B"])
model.ub = pyo.Param(model.S, initialize={"A": 4, "B": 7})
model.x = pyo.Var(model.S, domain=pyo.NonNegativeReals)
Alternative 1: an explicit rule.
def capacity_rule(m, s):
return m.x[s] <= m.ub[s]
model.capacity = pyo.Constraint(model.S, rule=capacity_rule)
Alternative 2: a decorator. Start from the setup again; do not execute both alternatives on the same model.
@model.Constraint(model.S)
def capacity(m, s):
return m.x[s] <= m.ub[s]
Both create model.capacity["A"] and model.capacity["B"], representing
x[A] <= 4 and x[B] <= 7. Notice the names: the explicit form chooses the
component name on the left of =, whereas the decorator uses the function’s
name. Calling the decorated function capacity_rule would create
model.capacity_rule, not model.capacity.
Read the decorator example in four steps:
model.Constraint(model.S)obtains a decorator attached to this model and passesmodel.Sas the constraint’s index set.Python defines the function
capacityand passes it to that decorator.Pyomo constructs a
Constraintwithrule=capacityand attaches it tomodelunder the namecapacity.For this concrete model, construction calls the rule once for each index, passing the model and that index. The returned relations become constraints.
The argument m is a local parameter receiving the model (or block) that owns
the component. Its name is your choice; it is not a special keyword. Use it
consistently inside the rule so the rule refers to the object Pyomo passes in.
The outside name model identifies the object receiving the component.
Pyomo returns the original function from this decorator: capacity remains a
Python function, while model.capacity is the constraint component. You normally
work with the latter after construction. No solver is called by the decorator.
For an AbstractModel, rules are evaluated when a concrete instance is created.
Scalar constraints, objectives and expressions#
The parentheses are needed even when there is no index set. Continuing with either indexed-constraint alternative above:
@model.Constraint()
def total_limit(m):
return sum(m.x[s] for s in m.S) <= 8
@model.Expression()
def revenue(m):
return 3 * m.x["A"] + 2 * m.x["B"]
@model.Objective(sense=pyo.maximize)
def profit(m):
return m.revenue
These create model.total_limit, model.revenue and model.profit.
sense=pyo.maximize is passed to the objective constructor; it is not an
argument to profit. Without it, the objective defaults to minimization.
An expression names a formula; it neither constrains nor optimizes that formula
by itself. Short scalar declarations can also be clear without a rule:
# An alternative to the total_limit decorator, not an additional declaration:
model.total_limit = pyo.Constraint(expr=sum(model.x[s] for s in model.S) <= 8)
Several index sets and conditional constraints#
In a transportation model, a rule over two one-dimensional sets receives the owning model followed by a source and a destination. This is a separate, self-contained example:
transport = pyo.ConcreteModel()
transport.SOURCES = pyo.Set(initialize=["factory"])
transport.DESTINATIONS = pyo.Set(initialize=["shop", "depot"])
transport.ship = pyo.Var(
transport.SOURCES, transport.DESTINATIONS,
domain=pyo.NonNegativeReals,
)
transport.capacity = pyo.Param(transport.SOURCES, initialize={"factory": 10})
transport.demand = pyo.Param(
transport.DESTINATIONS, initialize={"shop": 6, "depot": 0}
)
@transport.Constraint(transport.SOURCES)
def supply_limit(m, src):
return sum(m.ship[src, dst] for dst in m.DESTINATIONS) <= m.capacity[src]
@transport.Constraint(transport.DESTINATIONS)
def meet_demand(m, dst):
return sum(m.ship[src, dst] for src in m.SOURCES) >= m.demand[dst]
@transport.Constraint(transport.SOURCES, transport.DESTINATIONS)
def route_limit(m, src, dst):
if dst == "depot":
return pyo.Constraint.Skip
return m.ship[src, dst] <= 8
The last rule is called for each source-destination pair, but creates no
constraint for the depot. Constraint.Skip is a sentinel: return it without
parentheses. Every branch must return a relation or an appropriate Pyomo
sentinel; falling off the end returns None, which is not a valid constraint.
For tuple-valued index sets, flattened index entries may contribute several
arguments to the rule; its signature must match the index structure.
Here demand is a minimum service requirement, hence >=. Use == for exact
delivery or <= for a sales cap. Decorator syntax does not determine the
inequality direction. Similarly, the if above tests known data. A Python
if involving an unknown decision variable cannot express a solver’s choice;
use an appropriate algebraic or disjunctive formulation instead.
Inspecting and debugging the result#
After running the first example and its scalar extensions, inspect the model:
model.capacity.pprint()
print(model.capacity["A"].expr)
print(model.profit.expr)
print(list(model.component_map(pyo.Constraint)))
The last line lists capacity and total_limit. This inspects construction;
decision values are still unset until assigned or obtained from a successful
solve. Evaluate a solution with pyo.value(model.profit) only after checking
solver termination.
Common mistakes to check are:
Write
@model.Constraint(...)on a model or block instance, not@pyo.Constraint(...). The latter is the component class constructor.Pass the function itself with
rule=capacity_rule, not its result withrule=capacity_rule().Match the rule arguments to the index sets and return a valid expression.
Give components distinct names. Rerunning a declaration on the same model can replace a component; rerun the model-building setup for a clean example.
Define referenced sets, parameters and variables before constructing rules on a concrete model. Rules build symbolic expressions; they are not callbacks that the solver repeatedly executes during optimization.
Both styles are supported. Prefer decorators when they make the model easier
to read; use explicit rule= when attaching an existing reusable function or
when it better fits the surrounding code. Other components also support
decorators, but their rule return values and constructor options differ.
Rules for indexed bounds: the same idea, a different keyword#
A rule is simply a callable that Pyomo invokes with the appropriate arguments.
It need not be decorated. In particular, a variable’s bounds= callback returns
a pair (lower_bound, upper_bound), rather than a constraint relation:
bounded = pyo.ConcreteModel()
bounded.S = pyo.Set(initialize=["A", "B"])
bounded.limit = pyo.Param(bounded.S, initialize={"A": 4, "B": 7})
def variable_bounds(m, s):
return (0, m.limit[s])
bounded.x = pyo.Var(bounded.S, bounds=variable_bounds)
Thus bounded.x["A"].bounds is (0, 4) and the bounds for B are (0, 7).
An anonymous function is a shorter alternative for a small rule:
# Alternative to the preceding x declaration, on a fresh setup:
bounded.x = pyo.Var(bounded.S, bounds=lambda m, s: (0, m.limit[s]))
lambda m, s: ... defines a function; it is not a decorator. Both named and
anonymous callbacks are passed without calling them. Use None for a missing
bound. Bounds must not depend on unfixed decision variables: a relationship
such as x[s] <= y[s] belongs in a constraint. For known limits on a single
variable, bounds usually express the intent more directly than extra constraints.
The callback runs during variable construction, not at each solver iteration.
It may return expressions involving mutable parameters, which can subsequently
change value. Calling pyo.value() inside it instead would capture a number;
changing the parameter later would not update that captured number. Re-executing
the Python callback and evaluating a stored bound expression are different things.
Blocks: a rule builds a group of components#
A block groups related variables, constraints and expressions. For a block decorator, the first rule argument is the new block being constructed, not its parent model. An indexed block has one such object per index:
plant = pyo.ConcreteModel()
plant.PERIODS = pyo.Set(initialize=[1, 2])
plant.limit = pyo.Param(plant.PERIODS, initialize={1: 4, 2: 7})
@plant.Block(plant.PERIODS)
def period(b, t):
parent = b.parent_block()
b.x = pyo.Var(domain=pyo.NonNegativeReals)
@b.Constraint()
def capacity(local):
return local.x <= parent.limit[t]
@b.Expression()
def cost(local):
return 3 * local.x
The resulting names include plant.period[1].x,
plant.period[1].capacity and plant.period[2].cost. Here b.parent_block()
finds the immediate containing block, while b.model() finds the root model;
they coincide in this example but need not coincide in a nested hierarchy.
The nested constraint’s first argument receives the same local block that
owns that constraint.
Unlike a constraint rule, this block rule populates the block and needs no
return statement. The explicit alternative is to define the same function
without the decorator and use
plant.period = pyo.Block(plant.PERIODS, rule=period).
A block is organizational: it does not automatically decompose the optimization
problem or solve each period independently.
Parameters and the different meanings of a rule’s return value#
For a parameter, a decorated function supplies initial data. For example:
@plant.Param(plant.PERIODS)
def unit_cost(m, t):
return 2 + t
The explicit equivalent uses initialize=, not rule=:
plant.unit_cost = pyo.Param(plant.PERIODS, initialize=unit_cost) on a fresh
setup without the decorated declaration. Pyomo adapts the decorator’s rule
to parameter initialization. It is therefore useful to distinguish the
general callback idea from the constructor keyword appropriate to each component.
Purpose |
Typical declaration |
What the callable does |
|---|---|---|
Indexed constraint |
|
Returns a relation, a bound-expression tuple, or a constraint sentinel. |
Objective |
|
Returns the expression to optimize. |
Named expression |
|
Returns a symbolic expression. |
Parameter data |
|
Returns an initial parameter value. |
Group of components |
|
Populates each new block with components. |
Variable bounds |
|
Returns a lower/upper bound pair; no component decorator is needed. |
Where to see these patterns in the book#
1.2 A basic Pyomo model: start with scalar objective and constraint decorators in Steps 5 and 6.
1.3 A data-driven Pyomo model: compare the indexed
Paramdecorators, thebounds=lambda ...declarations for resources and products, and thematerials_usedconstraint indexed by resources. They illustrate three different uses of callbacks in one model.3.7 BIM production revisited: the block formulation groups decisions by period with
@m.Block(m.T); look for the nested@b.Constraintand@b.Expressiondeclarations.5.1 Milk pooling and blending:
@m.Block(m.C)andmccormick(b, c)create a group of McCormick constraints for each customer. This is a concrete use of the block argument andb.model().Functional Programming with Pyomo: the production-model example combines bounds callbacks and indexed constraints inside a model-building function, making repeated model construction explicit.
For the underlying Python semantics, see the Python reference on function definitions. For the component rules, see Pyomo’s constraints and objectives documentation. The introduction in Section 1.2 provides a shorter first encounter.
Naming conventions#
The choice of constraint and variables names is important for readable Pyomo models. Good practice is to use descriptive lower case names with words separated by underscores consistent with PEP 8 recommendations.
Pyomo models commonly use alternative conventions to enhance readability by visually distinguishing components of a model.
Prefer short model and block names#
Model and block names should be consistent with PEP 8 naming standards (i.e., all lowercase with words separated by underscore). Short model names are preferred for readability and to avoid excessively long lines. A single lower case m is acceptable in instances of a model with a single block.
Complex models may require more descriptive names for readability.
Set and RangeSet names may be all caps#
Consistent with common mathematical conventions in optimization modeling, use of upper-case names to denote Pyomo sets is an acceptable deviation from PEP style guidelines. Corresponding lower case name can then be used to denote elements of the set. For example, the objective
may be implemented as
import pyomo.environ as pyo
m = pyo.ConcreteModel()
m.MACHINES = pyo.Set(initialize=["A", "B", "C"])
m.finish_time = pyo.Var(m.MACHINES, domain=pyo.NonNegativeReals)
@m.Objective(sense=pyo.minimize)
def total_time(m):
return sum(m.finish_time[machine] for machine in m.MACHINES)
Parameter names may be capitalized#
Parameter names, especially mutable parameters intended for use in parametric studies, may use capitalized words (i.e., “CamelCase”).
Use descriptive Constraint and Variable names#
Objectives, constraints, variables, disjuncts, and disjunctions should use descriptive names following PEP 8 guidelines with lower case words separated by underscore (i.e, “snake_case”).
As an exception for small tutorial examples where mathematical formulation accompanies the model, the corresponding Pyomo model may use the same variable and parameter name. For example, a mathematical model written as
may be encoded as
import pyomo.environ as pyo
# create model instance
m = pyo.ConcreteModel()
# decision variables
m.x = pyo.Var(domain=pyo.NonNegativeReals)
m.y = pyo.Var(domain=pyo.NonNegativeReals)
# objective
m.f = pyo.Objective(expr = 40*m.x + 30*m.y, sense=pyo.maximize)
# declare constraints
m.a = pyo.Constraint(expr = 2*m.x + m.y <= 10)
m.b = pyo.Constraint(expr = m.x + 2*m.y <= 15)
m.pprint()
This practice is generally discouraged, because the resulting models are not easily read without reference to the accompanying mathematical notes. Pyomo includes a .doc attribute that can be used to document relationships between the Pyomo model and any reference materials.
import pyomo.environ as pyo
# create model instance
m = pyo.ConcreteModel()
# decision variables
m.production_x = pyo.Var(domain=pyo.NonNegativeReals, doc="x")
m.production_y = pyo.Var(domain=pyo.NonNegativeReals, doc="y")
# objective
m.profit = pyo.Objective(expr = 40*m.production_x + 30*m.production_y, sense=pyo.maximize)
m.profit.doc = "f"
# declare constraints
m.labor_a = pyo.Constraint(expr = 2*m.production_x + m.production_y <= 10, doc="A")
m.labor_b = pyo.Constraint(expr = m.production_x + 2*m.production_y <= 15, doc="B")
m.pprint()
Data styles and conventions#
Reading, manipulating, and writing data sets often consumes a considerable amount of time and coding in routine projects. Standardizing on a basic set of principles for organizing data can streamline coding and model development. Below we promote the use of Tidy Data for managing data sets associated with Pyomo models.
Use Tidy Data#
Tidy data is a semantic model for of organizing data sets. The core principle of Tidy data is that each data set is organized by rows and columns where each entry is a single value. Each column contains all data associated with single variable. Each row contains all values for a single observation.
scenario |
demand |
Price |
|---|---|---|
high |
200 |
20 |
medium |
100 |
18 |
low |
50 |
15 |
Tidy data may be read in multiple ways. Pandas DataFrame objects are well suited to Tidy Data and recommended for reading, writing, visualizing, and displaying Tidy Data.
When using doubly nested Python dictionaries, the primary keys should provide unique identifiers for each observation. Each observation is a dictionary. The secondary keys label the variables in the observation, each entry consisting of a single value.
scenarios = {
"high": {"demand": 200, "price": 20},
"medium": {"demand": 100, "price": 18},
"low": {"demand": 50, "price": 15}
}
Alternative structures may include nested lists, lists of dictionaries, or numpy arrays. In each case a single data will be referenced as data[obs][var] where obs identifies a particular observation or slice of observations, and var identifies a variable.
Multi-dimensional or multi-indexed data#
Pyomo models frequently require \(n\)-dimensional data, or data with \(n\) indices. Following the principles of Tidy Data, variable values should appear in a single column, with additional columns to uniquely index each value.
For example, the following table displays data showing the distance from a set of warehouses to a set of customers.
Customer 1 |
Customer 2 |
Customer 3 |
|
|---|---|---|---|
Warehouse A |
254 |
173 |
330 |
Warehouse B |
340 |
128 |
220 |
Warehouse C |
430 |
250 |
225 |
Warehouse D |
135 |
180 |
375 |
The distance variable is distributed among multiple columns. Reorganizing the data using Tidy Data principles results in a table with the all values for the distance variable in a single column.
Warehouse |
Customer |
Distance |
|---|---|---|
Warehouse A |
Customer 1 |
254 |
Warehouse B |
Customer 1 |
340 |
Warehouse C |
Customer 1 |
430 |
Warehouse D |
Customer 1 |
135 |
Warehouse A |
Customer 2 |
173 |
Warehouse B |
Customer 2 |
128 |
Warehouse C |
Customer 2 |
250 |
Warehouse D |
Customer 2 |
180 |
Warehouse A |
Customer 3 |
330 |
Warehouse B |
Customer 3 |
220 |
Warehouse C |
Customer 3 |
225 |
Warehouse D |
Customer 3 |
375 |
When working with multi-dimensional data, or other complex data structures, special care should be taken to factor “data wrangling” from model building.
Use Pandas for display and visualization#
The Pandas library provides an extensive array of functions for the manipulation, display, and visualization of data sets.
Acknowledgements#
This document is the result of interactions with students and colleagues over several years. Several individuals reviewed and provided feedback on early drafts and are acknowledged here.
David Woodruff, UC Davis
Javier Salmeron-Medrano, Naval Postgraduate School
Bethany Nicholson, John Siirola, Michael Bynum, and the Pyomo development team
Jasper M. H. van Doorn
Leon Lan