Mutability#

General guidance in rateslib is to create objects and not attempt to mutate them directly, either by overwriting or creating class attributes. For example, don’t create a FixedRateBond and then attempt to change that bond’s effective, or termination date or frequency. Instead, just create a new instance with the relevant modifications to input arguments.

However, Rateslib does not specifically place mutability guards on objects and adopts the general philosophy of Python’s flexible nature.

Certain objects are defined as mutable by updates, and these have dedicated methods to allow the types of mutations users will typically want to perform.

Mutable by update#

There are the following key objects with update methods, and defined as being mutable by update.

The first four of these are designed to be directly mutated by a Solver. This means that the Solver will overwrite the objects’ values with its own updates. The Solver’s updates will also utilise AD and the variables associated with the Solver, meaning it will destroy any user input variables when it runs an iteration.

The latter in this list also defines its own AD and variables and will ignore user variables.

Warning

Exogneous Variable should not be used with these objects due to their stated nature of being mutated and overwritten by the Solver and or its internals.

Splines are also mutable with a dedicated csolve() method to calibrate them to datapoints.

Mutable by association#

The following objects are defined as mutable by association, since they function as containers for mutable by update objects, or in the case of the latter are derived from a container.

The only one of these objects that contains an update method is FXForwards.update, and only to be backwards compatible when state management was not automatic in earlier versions of rateslib. This method offers the convenience of updating multiple FXRates objects via a single call, but it is no longer necessary.

Object states and the cache#

Internally, objects maintain a record of their state, and may also keep a cache.

In [1]: curve = Curve({dt(2025, 1, 1): 1.0, dt(2026, 1, 1): 0.97})

In [2]: _ = (curve[dt(2025, 2, 1)], curve[dt(2025, 8, 1)])

In [3]: curve._state
Out[3]: 8086936292820549114

In [4]: curve._cache
Out[4]: 
OrderedDict([(datetime.datetime(2025, 2, 1, 0, 0), 0.9974163968731297),
             (datetime.datetime(2025, 8, 1, 0, 0), 0.9824641982860269)])

When officially updated, their state will change and this will also clear the cache.

In [5]: curve.update_node(dt(2026, 1, 1), 0.98)

In [6]: curve._state
Out[6]: 7041991199639236262

In [7]: curve._cache
Out[7]: OrderedDict()

When methods on mutable by association objects are called they will perform a validation, and update themselves if they detect one of their contained objects has changed state, to ensure that erroneous results do not feed through.

In [8]: fxr = FXRates({"eurusd": 1.10}, settlement=dt(2025, 1, 5))

In [9]: fxf = FXForwards(fx_rates=fxr, fx_curves={"eureur": curve, "usdusd": curve, "eurusd": curve})

In [10]: fxf.rate("eurusd", dt(2025, 2, 1))
Out[10]: <Dual: 1.100000, (fx_eurusd), [1.0]>

In [11]: fxr.update({"eurusd": 1.20})  #  <-  the FXRates object is updated

In [12]: fxf.rate("eurusd", dt(2025, 2, 1))  #  <-  should auto-detect the new state
Out[12]: <Dual: 1.200000, (fx_eurusd), [1.0]>

Immutables#

Objects such as Calendars (Cal, UnionCal, NamedCal) are considered immutable, as well Number types (Dual, Dual2, Variable) and a Schedule.

Instruments#

Instances of Instruments, Legs and Periods should not be considered user mutable.

Internally, they do contain routines for setting mid-market prices on unpriced varieties. For example an IRS, which has no fixed_rate set at initialisation, or an FXCall, whose strike is indefinitely set as a delta-% at initialisation, will have its parameters definitively attributed for pricing and risk. These changes are automatically controlled.

Solver safeguards#

The Solver is a central component used for pricing and risk calculation. It also keeps track of the state of the objects within its scope, since without doing so errors may be inadvertently introduced.

Two examples are shown below. The first example updates the same Curve with different Solvers and demonstrates the generated error message.

In [13]: curve = Curve({dt(2025, 1, 1): 1.0, dt(2026, 1, 1): 1.0})

In [14]: solver1 = Solver(curves=[curve], instruments=[IRS(dt(2025, 1, 1), "1m", spec="usd_irs", curves=curve)], s=[1.0])
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 3.060634372867913e-14, `time`: 0.0017s

In [15]: solver2 = Solver(curves=[curve], instruments=[IRS(dt(2025, 1, 1), "1m", spec="usd_irs", curves=curve)], s=[5.0])
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 6.408017069158964e-15, `time`: 0.0016s

# solver2 has updated the curve after solver1 did. Try to price with solver1...
In [16]: try:
   ....:     IRS(dt(2025, 1, 1), "2m", spec="usd_irs", curves=curve).rate(solver=solver1)
   ....: except ValueError as e:
   ....:     print(e)
   ....: 
The `curves` associated with `solver` have been updated without the `solver` performing additional iterations.
In particular the object with id: '1bfb7' contained in solver with id: '4b153_' is detected to have been mutated.
Calculations are prevented in this state because they will likely be erroneous or a consequence of a bad design pattern.

In this second example a user calls an update method and adjusts some market data (or perhaps directly mutates a Curve) but does not reiterate the Solver.

In [17]: curve = Curve({dt(2025, 1, 1): 1.0, dt(2026, 1, 1): 1.0})

In [18]: fxr = FXRates({"eurusd": 1.10}, settlement=dt(2025, 1, 5))

In [19]: fxf = FXForwards(fx_rates=fxr, fx_curves={"eureur": curve, "usdusd": curve, "eurusd": curve})

In [20]: solver1 = Solver(curves=[curve], instruments=[IRS(dt(2025, 1, 1), "1m", spec="usd_irs", curves=curve)], s=[1.0], fx=fxf)
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 3.060634372867913e-14, `time`: 0.0021s

# user updates the FXrates
In [21]: fxr.update({"eurusd": 1.20})

# Try to price with solver1...
In [22]: import warnings

In [23]: with warnings.catch_warnings(record=True) as w:
   ....:     IRS(dt(2025, 1, 1), "2m", spec="usd_irs", curves=curve).rate(solver=solver1)
   ....:     print(w[-1].message)
   ....: 
The `fx` object associated with `solver` having id '59d65_' has been updated without the `solver` performing additional iterations.
Calculations can still be performed but, dependent upon those updates, errors may be negligible or significant.