ops library 2.11.0 released: removing defer() from events that shouldn't be deferred, and more

Happy Leap Day! The Charm Tech team has just released version 2.11.0 of ops.

It’s available from PyPI by using pip install ops, which should pick up the latest version (pin with ops==2.11.0 if you want to pin to a specific version).

In addition to smaller improvements to docs and tooling and some bug fixes, there’s one main change to be aware of:

defer() disabled on events that shouldn’t be deferred

For some events, there’s never a reason to defer() - for example, with ActionEvent, you’re communicating with the user (normally) in real-time, so waiting until the next event comes along to finish executing the event makes no sense. With a SecretExpiredEvent, Juju will keep firing the event until the charm indicates that it’s done the work (calling remove_revision), so again defer() doesn’t make sense.

There were a handful of events that similarly didn’t make sense, but still permitted calling defer(), and we’ve adjusted these in this release:

  • StopEvent - there are an extremely small number of events that the unit will get after this, so defer() is too risky to be worthwhile. If you can’t do what you need to, then it’s better to just have the remove handle the work.
  • RemoveEvent - speaking of remove, there are no more events after this for the departed unit, so anything that’s in the deferred queue will have no opportunity to run. The pod or machine is about to vanish, so if you need something on it, you’ll need to block until you can do that (or go into error state), and if you need to do cleanup outside the unit but can’t do it now, then you’ll need another unit to handle that.
  • CollectStatusEvent - this runs at the end of every hook event, so there’s really no point deferring it - whatever status it ends up with will just be replaced by the execution at the end of the hook.
  • CommitEvent and PreCommitEvent - similarly, these are run with every hook event, so there’s no point queuing them to run at a different time.

We looked into actually removing the defer() method from the classes, but, unfortunately, couldn’t do this while keeping sufficient backwards compatibility. What we’ve done is made all of these events consistently raise RuntimeError if defer() is called - so your unit tests should catch any accidental use, and set the return type to NoReturn, which IDEs usually use by making any subsequent code (like a return) faded out, to show that it won’t be executed.


To aid in converting older style charms, and also to provide a convenient way to get a unique identifier, ActionEvent objects now have an .id attribute, which is the Juju ID (sometimes called the Action UUID, but not in UUID form in modern Juju) for the invocation of the action.


  • You can now compare Plan objects (with another Plan or with an appropriate dictionary), and create Plans from a dictionary, just as you can with Layers.
  • ops and ops.testing had some special-casing around processing the JUJU_REMOTE_APP environment variable during relation-broken events, to attempt to match Juju behaviour. The conclusion from the Juju team is that this should always be set during relation-broken events (with all supported versions of Juju) and so the special-casing in ops and Harness has been removed, so the remote app will always be available (e.g. on the relation object).

Read more in the full release notes on GitHub.

1 Like