Why is setting decimal.getcontext().prec globally risky in a long-running service?
answer
- Ambient settings nobody passed to the function
- Every thread gets its own copy
- Significant digits, not decimal places
- A with-block copies and restores
- An explicit object takes no ambient state
basics
~20 sgetcontext() returns the calling thread's own context, so a precision set at import applies to that thread only - new threads start from the default of 28 digits. It is also ambient state every later operation silently inherits, and prec counts significant digits, not decimal places.
solid answer
~40 sA decimal context is the arithmetic environment: `prec` (significant digits, default 28), the rounding mode, exponent limits, and the traps and flags for signals such as `Inexact` and `InvalidOperation`. `decimal.getcontext()` returns the **current thread's** context, and a thread that has not touched it gets a fresh copy of `DefaultContext`. So a module that sets `getcontext().prec = 6` at import changes arithmetic only in whichever thread executed the import; worker threads keep 28 digits and quietly compute different results. Two further traps: `prec` counts *significant* digits, so 123456.789 at prec 6 becomes 123457 rather than gaining two decimal places, and the setting is ambient - every later operation in that thread inherits it. Scope it with `decimal.localcontext()`, or drop the global entirely and call methods on an explicit `decimal.Context` object.
code
python · 15 linesimport threading
from decimal import Decimal, getcontext
getcontext().prec = 5
seen = {}
def worker():
seen['prec'] = getcontext().prec
seen['value'] = Decimal(1) / Decimal(3)
t = threading.Thread(target=worker)
t.start()
t.join()
print('main ', getcontext().prec, Decimal(1) / Decimal(3))
print('worker', seen['prec'], seen['value'])go deeper
Know that decimal arithmetic reads settings from a context, that the default allows 28 significant digits, and that prec counts significant digits rather than decimal places.
Explain that getcontext() is per thread, that prec applies to operation results and not to construction, and that localcontext() is a with-block that copies and restores. Name traps and flags.
Diagnose the cross-thread divergence from a first-hand angle: two code paths computing different results because only one thread saw the mutated context. Prefer explicit Context objects in the calculation path and trap Inexact in the test suite.
Set the policy: no library mutates the global context, the default is chosen once at startup if at all, and precision-critical arithmetic is written against a passed-in Context so it is deterministic and testable regardless of import order or thread.
### What a context is Decimal arithmetic never happens in a vacuum: every operation consults a context. A `decimal.Context` carries `prec` (how many significant digits a result may have, default 28), `rounding` (default `ROUND_HALF_EVEN`), the exponent limits `Emin` and `Emax`, and two parallel dictionaries: `traps`, saying which signals raise, and `flags`, recording which have occurred. `InvalidOperation`, `DivisionByZero` and `Overflow` are trapped by default; `Inexact` and `Rounded` are only flagged, so ordinary rounding does not interrupt anything. ### Trap one: prec is significant digits, not decimal places This is the misunderstanding that sends people to `prec` in the first place. Setting `prec = 2` does not give you two decimal places on money; it gives you two significant digits, so a result of 1234.56 collapses to 1.2E+3. And `prec` applies to *operation results*, not to construction: under `prec = 6`, `Decimal('123456.789')` is unchanged, while `Decimal('123456.789') + Decimal(0)` becomes `123457`. If what you want is a fixed number of decimal places, that is a quantizing operation with an explicit rounding mode, not a precision setting. ### Trap two: the context is thread-local, and threads do not inherit yours `getcontext()` returns a context belonging to the calling thread. A thread that has never called it receives a fresh copy of `decimal.DefaultContext`. That produces a genuinely nasty class of bug. Picture a ticket-triage bot that computes SLA credits: one module does `decimal.getcontext().prec = 6` at import, the request handler runs on the thread that performed the import and produces six-digit results, and the background sweep runs on a worker thread that still has 28 digits. The two paths compute the same amounts and disagree in the last places, and nothing in either traceback mentions precision. You can reproduce it in ten lines: set `prec = 5` on the main thread, start a thread, and have it print `getcontext().prec` - it reports 28. The documented way to change the *default* for a multi-threaded program is to modify `decimal.DefaultContext` before any threads are started, since new threads copy from it. That is still a process-wide decision and should be made once, in application startup code, never in a library. ### Trap three: it is ambient state Even single-threaded, a global precision is invisible coupling. Every later Decimal operation in that thread inherits it, including operations in code you did not write and in code that runs long after the module that set it. Unit tests pass or fail depending on import order. In async code the effect is sharper still: coroutines that interleave on one event loop all share that thread's context, so a coroutine that mutates it mid-calculation changes results for every other coroutine that resumes afterwards. Nothing about a `Decimal` value records the context it was computed under, so the evidence is gone by the time you look. ### What to do instead **Scope it.** `with decimal.localcontext() as ctx:` copies the current context, lets you set `ctx.prec` inside the block and restores the previous context on exit, exception or not. Since Python 3.11 you can pass the settings as keyword arguments - `with localcontext(prec=4, rounding=ROUND_DOWN):` - which is the tidiest form. **Or avoid the global entirely.** Build a `decimal.Context(prec=..., rounding=...)` once and call its methods: `ctx.create_decimal(text)`, `ctx.divide(a, b)`, `ctx.add(a, b)`. Those consult the object you pass, not the ambient one, so the arithmetic is deterministic no matter which thread runs it or what any other module has done. For a service whose correctness depends on the precision, this is the version worth writing. **Do not raise prec hoping for exactness.** More digits postpone rounding; they do not eliminate it, because one third has no finite base-10 expansion at any precision. If a calculation must not round, the answer is an exact rational type for the intermediates, not a bigger digit budget. ### Turning silence into noise The one genuinely good use of the context in a money codebase is diagnostic: run the money test suite inside a `localcontext` with `traps[Inexact] = True`, and any operation that silently rounds raises instead of returning a slightly wrong number. Reading `flags` after a calculation is the weaker variant - flags accumulate and are never cleared for you, so assert on them and reset deliberately. Both turn an invisible loss of precision into a test failure, which is where you want to find it.
- How do you change the decimal precision for every thread in a service?Modify `decimal.DefaultContext` during application startup, before any threads are created, because each new thread copies its context from that template. Do it once, in the entry point, never in a library module at import time. If only one calculation needs a different precision, prefer a `localcontext` block or an explicit `Context` object over changing the process-wide default at all.
- Does raising prec to 50 make a money calculation exact?No. Higher precision delays rounding, it does not remove it: a third has no finite base-10 expansion at any precision, so the division still rounds, just later. It also makes every operation slower and hides the problem further from where it starts. If intermediates must be exact, compute them as exact rationals and convert once at the end.
- What is the difference between a context's traps and its flags?`traps` decides whether a signal raises an exception; `flags` records that the signal occurred. InvalidOperation, DivisionByZero and Overflow are trapped by default, while Inexact and Rounded are only flagged. Flags accumulate and are never cleared automatically, so if you inspect them you must reset them yourself before the operation you care about.
- Why is the context a poor fit for asyncio code?Coroutines on one event loop run on the same thread and therefore share that thread's context. A coroutine that changes it and then awaits leaves every coroutine resuming afterwards computing under the new setting, and a `localcontext` block spanning an await has the same reach. Pass an explicit Context object and call its methods when concurrent coroutines need different precisions.
A global context is a workshop where someone changed the calibration on the shared measuring gauge. Everyone who walks in afterwards uses the new setting, nobody was told, and each new room gets a factory-fresh gauge instead.
saying these in an interview costs you the question
- Sets prec expecting a number of decimal places
- Assumes new threads inherit the caller's context
- Mutates getcontext() at import time in a library
- Raises prec hoping to make division exact
- Thinks the constructor honours the context precision
- Never mentions localcontext or an explicit Context object