Setting Python’s Decimal Context for All Threads

Python’s decimal module has concept of a “context”. This defines the default precision of new Decimal
s, how rounding works, and lots of other behaviour. Maths gets complicated!
It’s quite common to need to customize the decimal context, to control one of these features. As the decimal documentation states, contexts are per-thread:
Thegetcontext()
function accesses a different Context object for each thread. Having separate thread contexts means that threads may make changes (such asgetcontext().prec=10
) without interfering with other threads.
This isolation is good, but it also means that it’s an easily forgotten concern when adding threading. This could cause hard-to-discover arithmetic bugs. Oh no!
Thankfully, there’s a fix, as per the decimal documentation:
[For new threads] the new context is copied from a prototype context calledDefaultContext
. To control the defaults so that each thread will use the same values throughout the application, directly modify theDefaultContext
object. This should be done before any threads are started so that there won’t be a race condition between threads callinggetcontext()
.
It then has this example:
# Set applicationwide defaults for all threads about to be launched
DefaultContext.prec = 12
DefaultContext.rounding = ROUND_DOWN
DefaultContext.traps = ExtendedContext.traps.copy()
DefaultContext.traps[InvalidOperation] = 1
setcontext(DefaultContext)
# Afterwards, the threads can be started
t1.start()
t2.start()
t3.start()
...
This is great but it requires us to think through how we’d specify this context. What if you want to use one of the pre-defined contexts?
For example there’s decimal.BasicContext
. It’s pretty useful, as it’s a standard:
This is a standard context defined by the General Decimal Arithmetic Specification.
Here’s an adaptation of the previous example to pull all attributes from BasicContext
into DefaultContext
:
import copy
import decimal
# Ensure we use the basic context while the application is running.
# 1. Copy all its values to the DefaultContext object used for new threads
for name in inspect.signature(decimal.Context).parameters:
setattr(decimal.DefaultContext, name, getattr(decimal.BasicContext, name))
# 2. Use on the current thread
decimal.setcontext(decimal.DefaultContext)
This uses inspect.signature()
to find all the parameters a Context
takes. The previous snippet listed them all explicitly: prec
, rounding
, etc. Using inspection instead means that our code is robust to future expansions in the decimal
module.
In Django
In Django you can store Decimal
values with the DecimalField
database field. If you use it, you probably want to use this same context-setting technique. The best place to do so is inside an AppConfig.ready()
method:
# example/core/apps.py
import decimal
import inspect
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = "core"
verbose_name = "Core"
def ready(self):
# Ensure we use Decimal basic context while the application is
# running to make all Decimal math use consistent values
# 1. Copy all its values to the DefaultContext object used for new
# threads
for name in inspect.signature(decimal.Context).parameters:
setattr(decimal.DefaultContext, name, getattr(decimal.BasicContext, name))
# 2. Use on the current thread
decimal.setcontext(decimal.DefaultContext)
This runs at project startup. It’s possible your other apps or third party packages start threads at startup, so you should check if they do. You can do so by adding a threading.enumerate()
.
If you find other threads, you can try setting the context earlier. You can do this by pushing your app to the top of INSTALLED_APPS
. If that still doesn’t work, you could try moving the snippet to your settings file.
Learn how to make your tests run quickly in my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.