Setting Python’s Decimal Context for All Threads
Python’s decimal module has concept of a “context”. This defines the default precision of new
Decimals, 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:
getcontext()function accesses a different Context object for each thread. Having separate thread contexts means that threads may make changes (such as
getcontext().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 called
DefaultContext. To control the defaults so that each thread will use the same values throughout the application, directly modify the
DefaultContextobject. This should be done before any threads are started so that there won’t be a race condition between threads calling
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
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)
inspect.signature() to find all the parameters a
Context takes. The previous snippet listed them all explicitly:
rounding, etc. Using inspection instead means that our code is robust to future expansions in the
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
# 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
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.
If your Django project’s long test runs bore you, I wrote a book that can help.
One summary email a week, no spam, I pinky promise.
Tags: django, python