Py_DEBUG compile flag, assertion macros, the --with-pydebug configure option, and debugging allocator.
A CPython debug build is a development build of the interpreter with extra runtime checks enabled. It is slower than a normal release build, but it exposes bugs that would otherwise appear as random crashes, memory corruption, reference leaks, or undefined behavior.
Debug builds are used when modifying CPython itself, writing C extensions, investigating object lifetime bugs, or validating changes to the runtime.
83.1 What a Debug Build Is
A normal CPython release build is optimized for users.
A debug build is optimized for developers.
| Build type | Main goal |
|---|---|
| Release build | Fast execution |
| Debug build | Detect incorrect behavior |
| Sanitizer build | Detect low-level C and memory errors |
| Instrumented build | Collect profiling or coverage data |
A debug build enables extra checks inside the interpreter. These checks make assumptions explicit.
Examples:
object reference counts must remain valid
garbage collector state must remain consistent
memory allocator boundaries must be respected
interpreter locks must be used correctly
C API calls must receive valid objects
internal invariants must holdWhen one of these assumptions fails, the debug build should fail early.
83.2 Building CPython in Debug Mode
On Unix-like systems:
./configure --with-pydebug
make -j8Then run:
./pythonCheck the build:
import sys
print(sys.flags)
print(hasattr(sys, "gettotalrefcount"))A debug build usually exposes extra runtime information, including total reference count support.
On Windows, debug builds are usually created with the CPython Visual Studio project files or build scripts.
83.3 What --with-pydebug Enables
The --with-pydebug option changes the interpreter in several important ways.
| Feature | Effect |
|---|---|
| Assertions | Enables many internal assert() checks |
| Debug ABI | Uses debug-specific build configuration |
| Reference debugging | Tracks total reference count information |
| Allocator debugging | Helps detect memory misuse |
| Extra runtime checks | Validates object and interpreter invariants |
| Slower execution | Adds overhead for diagnostics |
A debug build changes enough behavior that it should be treated as a separate development configuration.
83.4 Assertions
CPython contains many C assertions.
Example shape:
assert(op != NULL);
assert(Py_REFCNT(op) > 0);
assert(Py_TYPE(op) != NULL);These assertions check conditions that should always hold if the interpreter is correct.
In a release build, many assertions are disabled. In a debug build, they remain active.
An assertion failure usually means one of three things:
the current change broke an invariant
an older bug has become visible
the assertion is too strong for a valid edge caseThe first case is the most common during development.
83.5 Reference Count Debugging
Reference counting is central to CPython.
Every owned reference must eventually be released. Every borrowed reference must remain valid for its documented lifetime. Every stolen reference must no longer be decref’d by the caller after ownership transfer.
A debug build can help find these mistakes.
Common reference bugs include:
| Bug | Result |
|---|---|
Missing Py_DECREF | Memory leak |
Extra Py_DECREF | Use-after-free or crash |
Missing Py_INCREF | Object freed too early |
| Decref borrowed reference | Corruption or crash |
| Ignoring error cleanup | Leaks on failure paths |
Example leak:
PyObject *name = PyUnicode_FromString("x");
if (name == NULL) {
return NULL;
}
return PyLong_FromLong(1);Here, name is never released.
Corrected version:
PyObject *name = PyUnicode_FromString("x");
if (name == NULL) {
return NULL;
}
Py_DECREF(name);
return PyLong_FromLong(1);Reference bugs are often invisible in short programs. Debug builds make them easier to expose under tests.
83.6 sys.gettotalrefcount
Debug builds commonly expose:
sys.gettotalrefcount()This returns the total number of live references known to the interpreter.
Example:
import sys
before = sys.gettotalrefcount()
for _ in range(1000):
object()
after = sys.gettotalrefcount()
print(before, after)This value is noisy. Many internal caches and temporary objects affect it. It is mainly useful in controlled repeated test runs.
The CPython test runner uses this idea for reference leak testing:
./python -m test -R 3:3 test_dictThe test is run several times. Warmup runs are ignored. Measured runs are compared.
83.7 Debug Memory Allocator
CPython has several allocator layers.
A debug build can wrap allocated memory with extra metadata and guard regions.
This helps detect:
writing before the start of a buffer
writing past the end of a buffer
using memory after it was freed
freeing memory with the wrong allocator
double-free mistakesYou can also enable allocator debugging with:
PYTHONMALLOC=debug ./python script.pyThis is useful even outside a full CPython debug build.
83.8 Object Lifetime Checks
A debug build helps expose object lifetime errors.
A typical object lifecycle is:
allocate memory
initialize object header
initialize object-specific fields
publish reference
use object
clear contained references
deallocate object
free memoryErrors often happen in partially initialized objects.
Example:
PyObject *obj = type->tp_alloc(type, 0);
if (obj == NULL) {
return NULL;
}
/* field initialization fails here */
return NULL;This leaks obj.
Correct cleanup:
PyObject *obj = type->tp_alloc(type, 0);
if (obj == NULL) {
return NULL;
}
if (init_fields(obj) < 0) {
Py_DECREF(obj);
return NULL;
}
return obj;Debug builds make these paths easier to test.
83.9 Garbage Collector Debugging
Objects that participate in cyclic garbage collection must follow strict rules.
A GC-tracked object must:
allocate using GC-aware allocation
initialize all reference fields correctly
be tracked only after it is valid
visit contained references in tp_traverse
clear contained references in tp_clear
untrack before unsafe deallocation
free using the matching GC allocatorWrong GC behavior can corrupt collection state.
Example requirements for a container type:
static int
MyType_traverse(MyType *self, visitproc visit, void *arg)
{
Py_VISIT(self->child);
return 0;
}
static int
MyType_clear(MyType *self)
{
Py_CLEAR(self->child);
return 0;
}The debug build helps detect invalid transitions in GC-tracked objects.
83.10 Debugging Extension Modules
Debug builds are useful for C extension authors too.
Extension bugs often appear as interpreter bugs because they corrupt shared runtime state.
Common extension mistakes:
returning NULL without setting an exception
returning non-NULL while an exception is set
using borrowed references after container mutation
mixing allocators
forgetting to handle failure paths
using Python APIs without the GILExample API contract bug:
static PyObject *
bad(PyObject *self, PyObject *args)
{
return NULL;
}Returning NULL means an exception must be set.
Correct shape:
static PyObject *
good(PyObject *self, PyObject *args)
{
PyErr_SetString(PyExc_RuntimeError, "operation failed");
return NULL;
}Debug builds make this kind of violation easier to detect.
83.11 Debug Builds and Performance
Debug builds are slower.
They add overhead through:
extra assertions
less aggressive optimization
debug allocator checks
reference tracking
larger object metadata in some modes
additional consistency checksDo not use debug builds for normal performance measurement.
For benchmarking CPython, use a release or optimized build.
Typical optimized build:
./configure --enable-optimizations
make -j8Use debug builds to find correctness bugs. Use optimized builds to measure speed.
83.12 Combining Debug Builds With Tests
The most common workflow is:
./configure --with-pydebug
make -j8
./python -m test test_gcFor broader validation:
./python -m test -j0For reference leak testing:
./python -m test -R 3:3 test_gcFor verbose failure inspection:
./python -m test -v -x test_gcA debug build and the test suite together form the baseline development environment for CPython internals work.
83.13 Debug Builds and GDB
A debug build is much easier to inspect with a native debugger.
Example:
gdb --args ./python script.pyInside GDB:
run
bt
frame 0
p opCPython also provides debugger helpers that make Python objects easier to inspect.
Typical useful commands include:
py-bt
py-list
py-printThese commands connect C stack frames to Python-level execution state.
83.14 Debug Builds and Sanitizers
Debug builds and sanitizers solve related but different problems.
| Tool | Best at finding |
|---|---|
| Debug build | CPython invariant violations |
| AddressSanitizer | Use-after-free, buffer overflow |
| UndefinedBehaviorSanitizer | Undefined C behavior |
| ThreadSanitizer | Data races |
| Valgrind | Memory errors and leaks |
For deep runtime work, developers often use several configurations.
Example:
./configure --with-pydebug CFLAGS="-fsanitize=address"
make -j8This can expose C-level memory bugs that ordinary debug checks miss.
83.15 Common Debug Build Failures
A debug build may fail in places far from the actual bug.
Typical symptoms:
| Symptom | Likely cause |
|---|---|
Assertion failure in Py_DECREF | Object lifetime corruption |
| Crash in GC | Invalid container traversal or clear logic |
| Refleak in test runner | Missing decref on success or error path |
| Fatal Python error | Broken interpreter state |
| Allocator failure | Buffer overwrite or wrong allocator |
| Random later crash | Earlier memory corruption |
The first crash location is a clue, not always the root cause.
83.16 Practical Development Pattern
A reliable CPython workflow is:
make a small change
build with --with-pydebug
run focused tests
fix assertion failures first
run reference leak tests
run broader tests
use GDB for crashes
use sanitizers for memory corruption
benchmark only with optimized buildsThis keeps correctness and performance concerns separate.
83.17 Core Principle
A CPython debug build turns hidden assumptions into executable checks.
It makes the interpreter less forgiving, which is exactly what development needs. It catches invalid object state, bad reference ownership, memory allocator misuse, and broken runtime invariants before they become vague crashes in release builds.