skip to content

Where in your pipeline do you enable Python's development mode, and where must it stay off?

level: principalimportance: should knowfreq 22%

answer

  1. It is a bundle, not one feature
  2. Place each switch by cost and signal
  3. Failing beats printing in one place only
  4. Instrumented runs are not performance runs
  5. Deprecations are upgrade telemetry

basics

~20 s

On everywhere developers and CI run code, plus a dedicated instrumented staging run; off in production and in any environment used for performance measurement, because the debug allocator and asyncio debug mode add per-operation cost.

solid answer

~50 s

Treat it as a bundle you can unbundle. Local development and CI get the full `-X dev` — the slowdown is irrelevant there and the deprecation and `ResourceWarning` signal is the point — with the `error` action scoped to your own packages so a dependency's deprecation cannot red the build. A separate instrumented staging run, under a realistic load profile, is where you look for leaks and allocator misuse in C extensions. Production gets it off: the allocator debug hooks pad and pattern-fill every block, asyncio debug mode captures coroutine origins and logs slow callbacks, and both cost latency and memory for signal nobody is on call to act on. Take the individual switches you do want instead — `-X faulthandler` is cheap, and `PYTHONWARNINGS` can display deprecations without the rest. Set it through `PYTHONDEVMODE` so worker processes inherit it.

code

python · 6 lines
python
import sys

if sys.flags.dev_mode:
    print("development mode is on: instrumented run, not a latency measurement")
else:
    print("normal run")

go deeper

for a junior

Know the simple rule and the reason behind it: development mode belongs on your own machine and in CI, and stays off in production because it makes the interpreter slower. Being able to say which switch turns it on is enough here.

for a middle

Explain the specific costs — allocator padding and checks on every block, asyncio recording coroutine origins and callback durations — and know that the environment variable, not the flag, is what reaches child processes.

for a senior

Show the operating split you have actually run: full mode locally and in CI with warnings-as-errors scoped to your own code, an instrumented environment for leak hunts, uninstrumented for any performance number, and one cheap switch kept on in production.

for a principal

Own the tradeoff and the organisational consequence: which signal is worth which cost in each environment, how the ignore list stays a reviewed decision, and how deprecation counts from CI drive the interpreter-upgrade cadence rather than being noise a team learns to skip.

## The framing that makes this a judgement call Development mode is not one feature; it is a **bundle of independently available switches**: - the default warning filter, - the debug allocator, - `faulthandler`, - asyncio debug mode, - eager encoding-argument checks, - and a `sys.flags` bit. A lead's job is not to be for or against the bundle but to **place each part where its cost is worth its signal**. ## Local development: on, permanently The cost is invisible on a developer's machine and the payoff is immediate: - unclosed files report themselves, - deprecated calls appear as they are written rather than a year later, - and an allocator misuse in a C extension being integrated aborts at the fault instead of corrupting memory. `PYTHONDEVMODE=1` in the development container image or shell profile is the cheapest ergonomics win available. ## CI: on, with a policy about the `error` action Displaying warnings in CI output achieves nothing; nobody reads a green build's log. The value comes from **failing**, and the failure has to be one a team can act on. - Scope warnings-as-errors to your own modules — a deprecation raised inside a dependency you cannot patch should be recorded, not fatal. - And keep a small, explicitly reviewed ignore list with an owner and a removal date, so silencing is a decision rather than a habit. Refuse the tempting shortcut of quarantining the check when it goes red; the whole point is that it goes red before the interpreter upgrade does. ## A dedicated instrumented environment: on, deliberately Leak hunts and C-extension memory bugs need a run at a realistic load profile with the mode on and often `-X tracemalloc` alongside. Keep this as its own environment or its own job. Do not conflate it with the staging you use for performance rehearsal: an instrumented process cannot produce a trustworthy latency number, and a team that measures one instrumented and one not will chase **phantom regressions**. ## Production: off — and be specific about why - The **debug allocator** adds padding and fill patterns around every block and checks on every free; that is per-allocation overhead in the hottest path a Python process has, plus a memory increase. - **asyncio debug mode** records the origin of each coroutine and inspects callback durations, taxing exactly the code that was moved to asyncio for throughput. - Beyond cost there is the **signal problem**: warnings surfaced in production are advisory messages arriving in a channel meant for actionable alerts, and the honest response to almost all of them at 3 a.m. is nothing. If you want part of it, take the part: `-X faulthandler` is cheap enough to leave on permanently and converts a hard crash into a traceback, and `PYTHONWARNINGS` can display one specific deprecation you are tracking without any of the allocator cost. **Never run the `error` action in production**, where a dependency's deprecation would become an outage. ## Propagation is a design decision A `-X` flag applies to one process; the environment is inherited. Any real deployment has process trees — a supervisor, worker pools using the `spawn` or `forkserver` start method, tools shelling out — so use `PYTHONDEVMODE` when you mean "this whole tree" and the flag when you mean "this command". Have the process report `sys.flags.dev_mode` at start-up or on a health endpoint, so nobody has to guess which mode a container is running, and so a production process that somehow inherited the mode is visible rather than merely slow. ## Deprecations as upgrade telemetry The strategic argument for all of this is **version cadence**. Every `DeprecationWarning` your CI shows is a line that a future interpreter or dependency release intends to break, handed to you a release ahead of the breakage. - A team that surfaces them upgrades continuously in small increments; - a team that does not experiences each upgrade as an undiagnosed pile of failures and eventually stops upgrading, which is how a codebase ends up pinned to an unsupported interpreter with security consequences. That reframes the mode from a debugging toy into the cheapest input to a maintenance plan. ## Know the boundary Development mode surfaces resource, deprecation and import problems and instruments the allocator. It does not verify logic, numerics or types: a service quietly accumulating a floating-point rounding drift in a computed value gets no help from any of these switches. Keep the claim accurate when you sell the policy internally — overselling a diagnostic switch as a correctness guarantee is how it loses credibility the first time something ships broken with the mode enabled.

  • A team wants warnings-as-errors everywhere including production. How do you argue it?
    Agree on the goal, move the enforcement point. In CI, the `error` action is right because a failure costs a red build and a one-line fix. In production it converts an advisory message from a dependency you cannot patch into an outage, and the deprecation is not more urgent at peak than it was during the last build. Display and log them in production if the signal is wanted; keep the raising in CI, scoped to code the team owns.
  • Which single part of development mode would you keep in production, if any?
    `faulthandler`, via `-X faulthandler` or `PYTHONFAULTHANDLER`. It costs effectively nothing until the process dies, and it turns a bare signal into a Python traceback for every thread, which is often the only evidence you get from a crash inside a C extension. The allocator debug hooks and asyncio debug mode are the expensive parts and stay off; the warning filter is a separate, cheap decision you can make on its own.
  • How do you keep the instrumented environment from distorting capacity planning?
    Keep it separate and label it. Instrumented runs are for finding leaks and allocator misuse; capacity numbers come from an uninstrumented environment with production settings. Have the process announce `sys.flags.dev_mode` in its start-up log and on a health endpoint so any dashboard reading from it is unambiguous, and never compare a latency series across the boundary.
  • How does this policy interact with an interpreter-version upgrade plan?
    It is the input to it. Deprecations surfaced in CI are the concrete list of work a future release will force, available a version early, so the upgrade becomes a stream of small fixes instead of a project. Run the suite under the next release candidate with the mode on to get the list even earlier, and track the remaining count as the readiness signal rather than deciding by feel.

saying these in an interview costs you the question

  • Enabling development mode globally, production included
  • Treating warnings-as-errors as a production setting
  • Benchmarking latency on an instrumented process
  • Quarantining the CI check when deprecations turn it red
  • Assuming a `-X` flag reaches spawned worker processes
  • Selling the mode as a correctness or type check

context