Adam Johnson

Home | Blog | Training | Projects | Colophon

Setting Python's Decimal Context for All Threads

2020-03-23

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:

The 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 DefaultContext object. This should be done before any threads are started so that there won’t be a race condition between threads calling getcontext().

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 whilst 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 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 whilst 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.

Fin

Hope this helps you control your decimal arithmetic,

—Adam


Interested in Django or Python training? I'm taking bookings for workshops.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Tags: django, python