Skip to content

83. Debug Builds

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 typeMain goal
Release buildFast execution
Debug buildDetect incorrect behavior
Sanitizer buildDetect low-level C and memory errors
Instrumented buildCollect 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 hold

When 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 -j8

Then run:

./python

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

FeatureEffect
AssertionsEnables many internal assert() checks
Debug ABIUses debug-specific build configuration
Reference debuggingTracks total reference count information
Allocator debuggingHelps detect memory misuse
Extra runtime checksValidates object and interpreter invariants
Slower executionAdds 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 case

The 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:

BugResult
Missing Py_DECREFMemory leak
Extra Py_DECREFUse-after-free or crash
Missing Py_INCREFObject freed too early
Decref borrowed referenceCorruption or crash
Ignoring error cleanupLeaks 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_dict

The 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 mistakes

You can also enable allocator debugging with:

PYTHONMALLOC=debug ./python script.py

This 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 memory

Errors 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 allocator

Wrong 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 GIL

Example 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 checks

Do not use debug builds for normal performance measurement.

For benchmarking CPython, use a release or optimized build.

Typical optimized build:

./configure --enable-optimizations
make -j8

Use 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_gc

For broader validation:

./python -m test -j0

For reference leak testing:

./python -m test -R 3:3 test_gc

For verbose failure inspection:

./python -m test -v -x test_gc

A 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.py

Inside GDB:

run
bt
frame 0
p op

CPython also provides debugger helpers that make Python objects easier to inspect.

Typical useful commands include:

py-bt
py-list
py-print

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

ToolBest at finding
Debug buildCPython invariant violations
AddressSanitizerUse-after-free, buffer overflow
UndefinedBehaviorSanitizerUndefined C behavior
ThreadSanitizerData races
ValgrindMemory errors and leaks

For deep runtime work, developers often use several configurations.

Example:

./configure --with-pydebug CFLAGS="-fsanitize=address"
make -j8

This 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:

SymptomLikely cause
Assertion failure in Py_DECREFObject lifetime corruption
Crash in GCInvalid container traversal or clear logic
Refleak in test runnerMissing decref on success or error path
Fatal Python errorBroken interpreter state
Allocator failureBuffer overwrite or wrong allocator
Random later crashEarlier 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 builds

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