skip to content

Why does mutating decimal.getcontext().prec in one queue worker change later jobs' totals?

level: seniorimportance: should knowfreq 32%

answer

  1. Precision is state, not an argument
  2. Who else runs after your change?
  3. Exceptions skip a hand-rolled restore
  4. A with-block that copies and restores
  5. Keyword arguments to localcontext since 3.11

basics

~20 s

decimal.getcontext() returns shared mutable state, not a per-call setting, so a job that changes prec leaves it changed for everything that runs after it. Scope the change with decimal.localcontext(), which copies the context and restores it on exit.

solid answer

~50 s

`decimal.getcontext()` hands back the *current* context object, and mutating `prec` or `rounding` on it is a global change: every later decimal operation that reaches the same context sees it, including unrelated jobs the worker picks up next and other coroutines interleaving on the same event loop. It is also not exception-safe — an error between the change and the restore leaves the wrong precision in place. The fix is `decimal.localcontext()`, a context manager that copies the current context, applies your settings, and restores the previous one on exit, including on an exception; since Python 3.11 you can pass the attributes as keyword arguments, `localcontext(prec=6)`. Contexts are per thread, so a fresh worker thread starts from the default rather than inheriting your change — which makes this class of bug maddeningly intermittent. For fully explicit arithmetic, call the methods on a `Context` object you own.

code

python · 6 lines
python
from decimal import Decimal, getcontext, localcontext

getcontext().prec = 28
with localcontext(prec=6) as ctx:
    print(ctx.prec, Decimal(1) / Decimal(7))
print(getcontext().prec, Decimal(1) / Decimal(7))

go deeper

for a junior

Recall that decimal.getcontext() gives you shared settings rather than a per-call option, and that decimal.localcontext() is the with-block that changes precision or rounding for just one piece of code.

for a middle

Explain the copy-apply-restore mechanics, why the restore also happens on an exception, and that the current context is per thread so a new thread starts from the default instead of inheriting.

for a senior

Diagnose the symptom: totals changing in code that never touches the context, intermittent by job order. Show the narrow fix — scoped blocks or a per-call rounding argument — and know that awaiting inside a scoped block reopens the hole.

for a principal

Set the house rule: one deliberate context decision at start-up, no ambient mutation in library code, explicit Context objects where a component must not perturb its caller, and a test that fails when a module leaks a setting.

### What getcontext actually returns `decimal.getcontext()` does not return a snapshot or a copy. It returns *the* current `decimal.Context` object — a mutable object holding `prec`, `rounding`, the exponent limits, the trap settings and the accumulated signal flags — and every arithmetic operation on a Decimal reads it at the moment it runs. So `getcontext().prec = 6` is not a parameter you passed to a calculation; it is a change to shared state that the next calculation, in any function, will pick up. In a queue worker this is exactly the wrong shape. A job that widens precision for one delicate computation and never puts it back leaves every subsequent job on that thread computing at the wrong precision, and the symptom appears in totals produced by code that never mentions the decimal context at all. Because the change survives only in the thread that made it, the bug is order- and scheduling-dependent: it shows up when a particular job runs before another on the same worker, and vanishes the moment you reproduce it alone. A batch big enough to keep several workers busy — say a 2.4 GB working set of documents queued for conversion — reorders jobs across restarts, which is precisely the environment in which "the totals changed and nothing in that code changed" gets reported. ### Scoping the change properly `decimal.localcontext()` is the intended tool. It copies the current context on entry, gives you the copy to modify, and restores the previous context on exit — including when the block raises, which hand-rolled save-and-restore code routinely gets wrong: ```python from decimal import Decimal, getcontext, localcontext getcontext().prec = 28 with localcontext(prec=6) as ctx: print(ctx.prec, Decimal(1) / Decimal(7)) print(getcontext().prec, Decimal(1) / Decimal(7)) ``` That prints `6 0.142857` and then `28 0.1428571428571428571428571429`. Passing the attributes as keyword arguments requires Python 3.11 or later; before that you write `with localcontext() as ctx:` and set `ctx.prec = 6` on the first line of the block. Either form is safe. ### Threads, tasks and how far the change travels The current context is per thread: a newly started thread does not inherit the caller's context, it begins from a copy of the module's default. That is good isolation and bad diagnosis, because it means a leak in one worker thread is invisible from another. Coroutines are the harder case — they interleave *within* one thread, so an `await` in the middle of a block that has mutated the global context hands control to another coroutine that will run under your settings, and a coroutine that mutates the context can leave it changed for whatever resumes after it. This is the one place where `localcontext()` alone is not enough either: if you `await` inside the `with` block, other coroutines run inside your scoped context. Keep the block tight and free of awaits, or take the fully explicit route below. ### The explicit alternative A `decimal.Context` object is not only the ambient setting; it also exposes the arithmetic itself. `ctx = Context(prec=4)` followed by `ctx.divide(Decimal(1), Decimal(3))` computes under `ctx` with no reference to global state at all, and `ctx.create_decimal("1.23456789")` applies that context's precision at construction time — something the plain constructor never does. This style is verbose, which is why it is rare, but it is the right answer for a library that must not perturb its caller, and for computations that genuinely need a different precision while other work is in flight. ### Also global: the signal flags The same object accumulates flags — `Inexact`, `Rounded`, `Subnormal` and the rest — so if you ever inspect flags to prove a calculation was exact, you are reading shared state that some other code path may have set. Clear the flags immediately before the operation you are measuring, or do the whole check inside a `localcontext()` block so the flags you read are yours. Traps live there too: turning a trap on globally to catch a bug in one module changes exception behaviour everywhere in that thread. ### The rule to carry away Treat the decimal context the way you treat any process-wide setting: set it once at start-up if your whole application needs a non-default precision or rounding, and for anything narrower use `localcontext()` or an explicit `Context` object. Passing `rounding=` directly to a `quantize` call, where that is all you need, is better still — it changes no shared state at all.

  • Does starting a new thread inherit the decimal context of the thread that started it?
    No. The current context is per thread, and a freshly started thread begins from a copy of the module's default context rather than the parent's. That isolates workers from each other, but it also means a start-up tweak applied on the main thread does not reach your pool threads unless each thread sets it, and it makes a leaked setting invisible from any other thread.
  • Is localcontext() enough to protect a computation that awaits inside the block?
    Not on its own. An await inside the with-block yields to the event loop, so other coroutines run while your scoped settings are in force, and their own context changes can reach you. Keep the block tight and await-free, or drop the ambient context entirely and call the arithmetic methods on a Context object you own.
  • You want to prove a division was exact. Why is reading the context's Inexact flag risky?
    The flags live on the same shared context object and accumulate until something clears them, so a flag you read may have been set by unrelated code earlier on that thread. Clear the flag immediately before the operation you are measuring, or perform the operation and the check inside a localcontext block so the flags you inspect belong only to your code.

saying these in an interview costs you the question

  • Thinks getcontext returns a private copy
  • Restores precision by hand after the calculation
  • Assumes a new thread inherits the caller's context
  • Sets process-wide precision to fix one function
  • Believes an exception still restores the old context
  • Reads context flags without clearing them first

context