Every line of C code inside CPython, the reference implementation of Python, follows a precise set of rules defined in PEP 7. Written by Guido van Rossum and Barry Warsaw, this living document governs how the interpreter itself is built, and understanding it is essential for anyone who wants to contribute to CPython, write stable C extensions, or simply understand how Python works under the hood.
Many Python developers encounter PEP 8 early in their careers. It is the style guide for Python code, and linters, editors, and CI pipelines enforce it constantly. But PEP 7 is different. It operates one layer deeper, in the C source files that make up CPython itself. If PEP 8 governs how you write Python, PEP 7 governs how Python itself is written.
This article covers PEP 7 exhaustively, with real code examples drawn directly from its specification and from CPython's source. Whether you are planning a first contribution to CPython, building a C extension module, or simply satisfying your curiosity about how the interpreter is maintained, this guide gives you everything you need.
What Is PEP 7 and Why Does It Exist?
PEP 7 is a Process-type Python Enhancement Proposal with Active status. It was created on July 5, 2001, by Guido van Rossum and Barry Warsaw, and it has been updated several times since, most recently in August 2025. Unlike Informational or Standards Track PEPs, a Process PEP describes a procedure or convention that the Python community has agreed to follow. PEP 7 sets those conventions for all C code that forms the CPython runtime.
The motivation for a C style guide is straightforward: CPython's codebase spans hundreds of C files, has been touched by thousands of contributors over more than three decades, and must compile cleanly across Linux, macOS, Windows, and a variety of other platforms with compilers ranging from GCC to MSVC. Without a consistent style, the codebase would become difficult to read, maintain, and audit for security.
PEP 7 explicitly acknowledges that its own rules are meant to be broken when following them would hurt readability or conflict with the style of surrounding code. It frames both of these as valid reasons to deviate. — Paraphrased from PEP 7, Introduction
That caveat is important. PEP 7 is not a rigid bureaucratic mandate. It is a practical guide maintained by people who understand that real codebases evolve, accumulate historical debt, and sometimes require exceptions. The document explicitly acknowledges both of these scenarios as valid reasons to deviate. That intellectual honesty is one of the things that makes PEP 7 worth studying carefully.
C Standards: From C89 to C11
One of the most practically important sections of PEP 7 concerns which version of the C standard is in effect for any given Python version. This matters enormously because C features that are available in C11 may not compile at all with C89 rules, and CPython must support a wide range of compilers and targets.
PEP 7 divides Python's C standard history into three distinct eras:
Python 3.11 and Newer: C11
From Python 3.11 onward, CPython's internal implementation code targets C11 without optional features. Optional features in C11 include things like variable-length arrays (VLAs) and atomic operations beyond the basic memory model, and their absence ensures maximum portability. The public C API, however, must remain compatible with both C99 and C++ to allow extension authors to use either language.
For anything not covered by C11 or its supported subset, CPython uses internal wrapper macros and functions rather than relying on compiler-specific behavior. The PEP cites examples such as _Py_atomic_store_int32 for atomic operations, Py_ALWAYS_INLINE for inlining hints, and Py_ARITHMETIC_RIGHT_SHIFT for arithmetic right shifts, which are technically implementation-defined in standard C. When adding such wrappers, the goal is to make them easy to adjust if a compiler does not support the underlying behavior.
Python 3.6 to 3.10: C89 Plus Selected C99 Features
Before 3.11, CPython used a pragmatic hybrid: the base was C89 (ANSI C, the 1989 standard), but with a carefully chosen set of C99 additions that were reliable across the compilers CPython had to support. PEP 7 lists exactly which C99 features were permitted during this era:
- Standard integer types from
<stdint.h>and<inttypes.h>, with fixed-width types such asint32_tanduint64_trequired static inlinefunctions as a safe alternative to macros- Designated initializers, particularly useful for type declarations like
PyTypeObject - Intermingled declarations (variables declared anywhere in a block, not just at the top)
- The
booltype via<stdbool.h> - C++-style line comments using
//
If you are contributing to an older branch of CPython (3.10 or earlier), you must restrict yourself to C89 plus those specific C99 features. Introducing C11 constructs into a 3.10 maintenance branch would be a bug, not an improvement.
Python Before 3.6: Pure C89
Versions of CPython before 3.6 used strict ANSI/ISO C89. The most visible consequence was that all variable declarations had to appear at the top of a block, before any executable statements. This is still something many C programmers trained on modern standards find unfamiliar when reading older CPython source.
/* C89 style: all declarations at top of block */
static int
count_items(PyObject *list)
{
Py_ssize_t i;
Py_ssize_t n;
int count;
n = PyList_GET_SIZE(list);
count = 0;
for (i = 0; i < n; i++) {
count++;
}
return count;
}
/* C99/C11 style (Python 3.6+): declarations near first use */
static int
count_items(PyObject *list)
{
Py_ssize_t n = PyList_GET_SIZE(list);
int count = 0;
for (Py_ssize_t i = 0; i < n; i++) {
count++;
}
return count;
}
Common C Code Conventions
PEP 7 establishes a set of general conventions that apply throughout CPython regardless of C standard version. These are the rules that shape day-to-day code in the interpreter.
No Compiler-Specific Extensions
CPython must compile with GCC, Clang, MSVC, and other compilers across many platforms. PEP 7 explicitly forbids using compiler-specific extensions such as GCC's __attribute__ syntax or MSVC-specific pragmas in ways that are not wrapped by a portability macro. For example, writing a multi-line string without trailing backslashes is a GCC extension that MSVC does not support and is therefore prohibited.
/* WRONG: GCC extension, MSVC will reject this */
const char *message = "Hello,
world";
/* CORRECT: portable multi-line string using backslash continuation */
const char *message = "Hello, "
"world";
Full Function Prototypes
Every function declaration and definition must use a complete prototype that specifies the types of all parameters. Functions that take no arguments must explicitly declare (void), not an empty parameter list. This is a safety rule: in C, an empty parameter list () means the function accepts an unspecified number of arguments, which disables type checking at call sites.
/* WRONG: empty parameter list, no type checking */
static int initialize();
/* CORRECT: explicit void, no parameters accepted */
static int initialize(void);
/* CORRECT: full prototype with parameter types */
static PyObject *
create_object(PyTypeObject *type, PyObject *args, PyObject *kwds);
No Compiler Warnings
PEP 7 requires that code produce no warnings with the major compilers GCC, MSVC, and a few others. Warnings in C are frequently genuine bugs in disguise, particularly around implicit conversions, signed/unsigned mismatches, and uninitialized variables. CPython's CI infrastructure enforces warning-free builds, and patches that introduce warnings are not accepted.
Prefer Static Inline Functions Over Macros
In new code, static inline functions are preferred over macros wherever possible. This is a significant shift from older C conventions where macros were used for performance-critical operations. Inline functions offer type safety, proper scoping of arguments (avoiding double-evaluation bugs), and meaningful names in debuggers and stack traces, while compilers are generally as good at inlining them as they are at expanding macros.
/* Old-style macro approach: dangerous due to double-evaluation */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
/* Modern PEP 7 approach: static inline function */
static inline Py_ssize_t
Py_MAX(Py_ssize_t a, Py_ssize_t b)
{
return a > b ? a : b;
}
Code Layout Rules
PEP 7's layout rules govern everything from indentation to brace placement to line length. These rules ensure that CPython source files look consistent regardless of who wrote them or when.
Indentation and Line Length
Indentation uses four spaces. Tabs are not used at all. Lines must not exceed 79 characters. PEP 7 notes that if following both the indentation rule and the line length rule together leaves insufficient room to express the logic clearly, the code is probably too complicated and should be refactored into subroutines. Lines must also never end in whitespace.
Function Definition Style
Function definitions follow a specific layout that differs from many C style guides. The return type goes on its own line, the function name starts at column 1 (the leftmost character), and the opening curly brace also goes at column 1 on the same line as the function name. A blank line separates the local variable declarations from the first executable statement.
static int
extra_ivars(PyTypeObject *type, PyTypeObject *base)
{
int t_size = PyType_BASICSIZE(type);
int b_size = PyType_BASICSIZE(base);
assert(t_size >= b_size); /* type smaller than base! */
if (type->tp_itemsize || base->tp_itemsize) {
return 1;
}
if (t_size == b_size) {
return 0;
}
return 1;
}
Placing the function name at column 1 is a convention that predates modern editors. It originated partly because it makes it trivially easy to search for function definitions with tools like grep "^function_name", since only the definition starts at column zero while calls appear mid-line.
Brace and Spacing Rules
PEP 7 requires a space between keywords like if, for, while, and switch and the opening parenthesis that follows them. There are no spaces inside the parentheses. Braces are required everywhere C allows them to be omitted, but only in new code: you should not add braces to existing code you are not otherwise modifying.
/* WRONG: no space after keyword, missing braces */
if(condition)
do_something();
/* CORRECT: space after keyword, braces required in new code */
if (condition) {
do_something();
}
/* else goes on its own line, after the closing brace */
if (mro != NULL) {
use_mro(mro);
}
else {
handle_null_mro();
}
Notice that in PEP 7 style, the else keyword sits on its own line after the closing brace of the if block. This differs from both the K&R style used in many C projects (} else { on one line) and from PEP 8's Python style. It is unique to CPython's C conventions.
The Return Statement
PEP 7 explicitly forbids adding redundant parentheses to return statements. In C, return is a statement, not a function call, and parentheses around the returned value serve no purpose and add visual noise.
/* WRONG: redundant parentheses */
return(albatross);
/* CORRECT */
return albatross;
Function and Macro Call Style
Function calls use no space before the opening parenthesis, no spaces inside the parentheses, no spaces before commas, and one space after each comma. This applies consistently to both function calls and macro invocations.
/* WRONG */
foo( a,b ,c );
bar (x, y);
/* CORRECT */
foo(a, b, c);
bar(x, y);
Operators and Spacing
Spaces always surround assignment operators, Boolean operators, and comparison operators. In complex expressions involving many operators, additional spaces can be added around the outermost (lowest-priority) operators to make the precedence visually clear without relying on the reader to remember C operator precedence tables.
/* Spaces around all assignment and comparison operators */
int result = a + b;
int is_valid = (x > 0) && (y < max);
/* Extra space around lowest-priority operators in complex expressions */
z = a*a + b*b; /* multiplication binds tighter, add visual spacing */
z = (a+b) * (c+d); /* group low-priority addition with parens */
Breaking Long Lines
When a line exceeds 79 characters, PEP 7 provides guidance on where to break it. The preference is to break after commas in the outermost argument list, and continuation lines should be indented to align with the opening parenthesis of the call.
/* Breaking a long function call at the outermost argument list */
PyErr_Format(PyExc_TypeError,
"cannot create '%.100s' instances",
type->tp_name);
/* Breaking a long conditional: operator at start of continuation line */
if (type->tp_dictoffset != 0
&& base->tp_dictoffset == 0
&& type->tp_dictoffset == b_size
&& (size_t)t_size == b_size + sizeof(PyObject *))
{
return 0;
}
PEP 7 notes it is acceptable to put operators at the ends of lines, particularly to be consistent with surrounding code, and refers readers to PEP 8 for a longer discussion of this choice.
Macros
PEP 7 provides specific guidance for macros. Macros intended to be used as statements (rather than as expressions) must use the do { ... } while (0) idiom without a final semicolon. This ensures that the macro behaves correctly when used with or without surrounding braces and that the caller must supply the semicolon, making it look like a normal statement call.
/* CORRECT: do-while(0) idiom for statement macros */
#define ADD_INT_MACRO(MOD, INT) \
do { \
if (PyModule_AddIntConstant((MOD), (#INT), (INT)) < 0) { \
goto error; \
} \
} while (0)
/* Usage: caller adds the semicolon */
ADD_INT_MACRO(m, SOME_CONSTANT);
ADD_INT_MACRO(m, ANOTHER_CONSTANT);
PEP 7 also requires that multi-line macros vertically align their line continuation backslashes and that file-local macros be #undef'd after use to prevent name leakage.
Other Layout Points
Several additional layout rules apply throughout CPython. Blank lines should surround function definitions, structure definitions, and major sections inside functions. Comments always appear before the code they describe, never after or to the side of a complex expression. All functions and global variables must be declared static unless they are part of a published interface.
if / else block?else keyword on its own line after the closing brace of the if block. This is unique to CPython's C conventions and differs from both K&R style and PEP 8's Python style. New code always requires braces even for single-statement blocks.Naming Conventions
CPython's naming conventions for C are among the most distinctive aspects of PEP 7. Because CPython's public C API is used by every C extension module ever written, the naming rules also serve as a namespace management system, ensuring that CPython's symbols do not collide with user or third-party code. see API Stability
The Py Prefix System
Public functions that are part of CPython's public C API must begin with the Py prefix. Static functions (internal to a single file) must never use this prefix. The Py_ prefix (with an underscore) is reserved for global service routines like Py_FatalError and Py_DECREF. More specific APIs use longer prefixes that identify their module or type, such as PyObject_, PyList_, PyDict_, and PyUnicode_.
/* Public API functions: Py prefix, MixedCase with underscores */
PyObject *PyObject_GetAttr(PyObject *v, PyObject *name);
PyObject *Py_BuildValue(const char *format, ...);
PyObject *PyList_New(Py_ssize_t size);
PyObject *PyDict_GetItem(PyObject *mp, PyObject *key);
/* Internal function visible to the loader: _Py prefix */
void _PyObject_Dump(PyObject *op);
/* Static (file-local) function: no Py prefix */
static int
count_gc_objects(PyObject *container);
/* Public API declaration in a header file using the macro */
PyAPI_FUNC(PyObject *) PyObject_Repr(PyObject *);
PyAPI_DATA(PyTypeObject) PySuper_Type;
Naming Style for Functions and Variables
Public functions and variables use MixedCase with underscores separating major components. This might look like PyObject_GetAttr or Py_BuildValue. Exception objects follow the pattern PyExc_TypeError and PyExc_ValueError. If you are coming from the Python side, where the distinction between functions and methods follows different naming rules, the C API's MixedCase convention may feel unfamiliar at first.
Macro Naming
Macros follow a two-part naming convention. They begin with a MixedCase prefix that identifies the API group, then use ALL_CAPS for the remaining name. Examples from CPython include PyString_AS_STRING, Py_PRINT_RAW, and PyList_GET_SIZE. The intent is to make macros visually distinguishable from function calls, since macros may evaluate arguments more than once or have other behaviors that differ from regular function calls.
Macro parameters must themselves use ALL_CAPS style so they are easily distinguishable from C variables and struct members within the macro body.
/* Macro with MixedCase prefix + ALL_CAPS suffix */
/* Parameters in ALL_CAPS to distinguish from local C variables */
#define PyList_GET_ITEM(OP, I) ((PyListObject *)(OP))->ob_item[I]
#define Py_INCREF(op) \
do { \
PyObject *_py_incref_tmp = (PyObject *)(op); \
_py_incref_tmp->ob_refcnt++; \
} while (0)
/* Internal semi-private function: _Py prefix */
PyObject *_PyObject_GenericGetAttrWithDict(PyObject *obj,
PyObject *name,
PyObject *dict,
int suppress);
The _Py prefix for internal-but-visible functions is an important boundary. If you see a function starting with a single underscore like _PyObject_Dump, it is technically accessible but is considered internal to CPython and may change between versions without notice. Extension authors should not rely on _Py prefixed symbols in production code.
Documentation Strings in C
CPython's C code contains inline documentation strings that are exposed to Python through the __doc__ attribute of built-in functions and types. PEP 7 specifies exactly how these should be written.
Using PyDoc_STR and PyDoc_STRVAR
All docstrings in CPython C code must use either PyDoc_STR() or PyDoc_STRVAR() rather than raw string literals. The reason is that Python can be compiled with the --without-doc-strings configure option to reduce memory usage on resource-constrained systems. When that option is active, these macros expand to empty strings, eliminating the docstring storage entirely. Using a raw string literal would bypass this mechanism and embed the docstring unconditionally.
/* WRONG: raw string literal bypasses --without-doc-strings */
static PyMethodDef list_methods[] = {
{"append", list_append, METH_O, "Append object to the end of the list."},
{NULL, NULL}
};
/* CORRECT: use PyDoc_STR for inline docstrings */
static PyMethodDef list_methods[] = {
{"append", list_append, METH_O, PyDoc_STR("Append object to the end of the list.")},
{NULL, NULL}
};
/* CORRECT: use PyDoc_STRVAR for named docstring variables */
PyDoc_STRVAR(list_append__doc__,
"append($self, object, /)\n\
--\n\
\n\
Append object to the end of the list.");
Signature Lines
The first line of each function docstring must be a signature line that gives a brief synopsis of the arguments and return value. A blank line must separate this signature line from the description text that follows.
/* Correct docstring format with signature line */
PyDoc_STRVAR(myfunction__doc__,
"myfunction(name, value) -> bool\n\n\
Determine whether name and value make a valid pair.\n\
\n\
Returns True if the pair is valid, False otherwise.");
/* If the function always returns None, omit the return type indicator */
PyDoc_STRVAR(list_sort__doc__,
"sort($self, /, *, key=None, reverse=False)\n\
--\n\
\n\
Sort the list in ascending order and return None.");
Multi-Line Docstring Continuation
Multi-line docstrings in C require careful handling because C string literal concatenation and backslash continuation both work, but a raw multi-line string (without either technique) is not portable. MSVC in particular is known to reject raw multi-line string literals that span actual newlines in the source file. PEP 7 recommends using backslash continuation or string literal concatenation.
/* CORRECT: backslash continuation */
PyDoc_STRVAR(func__doc__,
"func(name, value) -> bool\n\n\
Determine whether name and value make a valid pair.");
/* CORRECT: string literal concatenation */
PyDoc_STRVAR(func__doc__,
"func(name, value) -> bool\n\n"
"Determine whether name and value make a valid pair.");
/* WRONG: raw multi-line literal, fails on MSVC */
PyDoc_STRVAR(func__doc__,
"func(name, value) -> bool\n\n
Determine whether name and value make a valid pair.");
Applying PEP 7 to Your Own C Extensions
PEP 7 is technically a guide for CPython's own C implementation, but it serves as an excellent reference for anyone writing C extension modules. Extensions that follow PEP 7 conventions look consistent alongside CPython's own code, are easier for other CPython contributors to review, and benefit from the same portability thinking that went into the PEP itself. If you are exploring ways to write Python that runs as C code, understanding PEP 7 conventions will give you a strong foundation for the C layer underneath.
Here is a complete, minimal C extension module that follows PEP 7 conventions throughout:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
/* PyDoc_STRVAR for the module-level docstring */
PyDoc_STRVAR(module_doc,
"A minimal C extension demonstrating PEP 7 style.\n\
\n\
This module exposes a single function, add_ints, which\n\
sums two C long integers and returns the result.");
/* PyDoc_STRVAR for the function docstring */
PyDoc_STRVAR(add_ints__doc__,
"add_ints(a, b) -> int\n\
\n\
Return the sum of two integers a and b.");
/* Static (file-local) helper: no Py prefix */
static long
_add_longs(long a, long b)
{
return a + b;
}
/* The Python-callable function */
static PyObject *
add_ints(PyObject *module, PyObject *args)
{
long a;
long b;
long result;
if (!PyArg_ParseTuple(args, "ll:add_ints", &a, &b)) {
return NULL;
}
result = _add_longs(a, b);
return PyLong_FromLong(result);
}
/* Method table: static, file-local */
static PyMethodDef example_methods[] = {
{"add_ints", add_ints, METH_VARARGS, add_ints__doc__},
{NULL, NULL, 0, NULL} /* sentinel */
};
/* Module definition */
static struct PyModuleDef example_module = {
PyModuleDef_HEAD_INIT,
"example", /* module name */
module_doc, /* module docstring */
-1, /* per-interpreter state size */
example_methods
};
/* Module init function: public, uses PyInit_ prefix */
PyMODINIT_FUNC
PyInit_example(void)
{
return PyModule_Create(&example_module);
}
Notice the #define PY_SSIZE_T_CLEAN at the top of the file. This is required before including Python.h for any code that passes format codes using # to functions like PyArg_ParseTuple. Omitting it can result in subtle type mismatch bugs on 64-bit platforms where Py_ssize_t and int differ in size.
Error Handling and the goto Cleanup Pattern
PEP 7 does not dedicate a formal section to error handling, but CPython's C code follows a deeply consistent pattern that anyone writing PEP 7-compliant code needs to understand: the goto cleanup idiom. In C, there are no exceptions, no destructors, and no garbage collector running behind your code. When a function allocates resources or acquires references to Python objects and then encounters an error partway through, it must release everything it acquired before returning. The goto cleanup pattern centralizes that teardown logic at the end of the function. see Reference Counting
The structure works like this: declare all PyObject * locals as NULL at the top of the function, perform work in sequence, and if any step fails, jump to a labeled cleanup block at the bottom that safely releases everything using Py_XDECREF (which tolerates NULL). The function returns NULL to propagate the error or returns a valid result if everything succeeded.
static PyObject *
build_greeting(PyObject *module, PyObject *args)
{
PyObject *name = NULL;
PyObject *fmt = NULL;
PyObject *result = NULL;
if (!PyArg_ParseTuple(args, "U:build_greeting", &name)) {
goto error;
}
fmt = PyUnicode_FromString("Hello, %s!");
if (fmt == NULL) {
goto error;
}
result = PyUnicode_Format(fmt, name);
if (result == NULL) {
goto error;
}
Py_DECREF(fmt);
return result;
error:
Py_XDECREF(fmt);
Py_XDECREF(result);
return NULL;
}
This pattern appears in hundreds of functions throughout CPython's source. It is not a stylistic preference; it is the standard approach to resource management in C code that interacts with the Python runtime. Every function that creates, borrows, or steals references to Python objects must account for early exits on every code path. The goto error idiom ensures there is exactly one place where cleanup happens, which makes auditing for leaks far simpler than scattering Py_DECREF calls across multiple return statements.
Do not call PyErr_SetString when the function you called has already set the exception. CPython's error model works like a per-thread error indicator: when a C API function fails and returns NULL or -1, it has already set the exception type and value. Your job is to clean up and propagate the error by returning NULL, not to overwrite the existing exception with a less specific one.
Reference Counting Discipline
Reference counting is the mechanism that determines when a Python object's memory can be freed, and getting it wrong in C code produces either memory leaks (forgotten Py_DECREF) or use-after-free crashes (premature Py_DECREF). PEP 7's style conventions interact directly with this reality. The requirement that all PyObject * locals be initialized to NULL, that cleanup be centralized, and that functions declare their variables at the top of the block all serve to make reference ownership visible and auditable. see Error Handling
CPython distinguishes between owned references (you must eventually Py_DECREF) and borrowed references (you must not). The C API documentation for each function specifies which kind it returns or expects. When a function returns a new reference, the caller owns it. When a function returns a borrowed reference, the caller must not decrement it unless it first calls Py_INCREF to take ownership.
/* Py_XDECREF: safe to call on NULL, used in cleanup paths */
Py_XDECREF(obj);
/* Py_DECREF: caller guarantees obj is not NULL */
Py_DECREF(obj);
/* Py_CLEAR: sets pointer to NULL before decrementing,
prevents dangling pointer if dealloc triggers code
that accesses the same variable */
Py_CLEAR(obj);
/* Py_NewRef (C API 3.10+): increments and returns,
useful for one-line owned-reference returns */
return Py_NewRef(existing_obj);
The style discipline matters here because reference counting bugs are among the hardest to diagnose in CPython C code. A leaked reference may never cause a visible crash; it just silently prevents an object from being freed, producing a slow memory leak that only surfaces under sustained load. A premature decrement may work fine for weeks until a specific garbage collection cycle triggers a use-after-free. Writing clean, consistent code in the PEP 7 style -- with NULL initialization, centralized cleanup, and explicit ownership tracking -- is the primary defense against these bugs.
Use Py_CLEAR(obj) instead of Py_DECREF(obj); obj = NULL; in any context where the object's deallocation might trigger arbitrary Python code that could re-enter the same function or access the same variable. This is especially relevant in tp_dealloc and tp_traverse implementations, where the garbage collector may be actively walking the object graph.
PyDict_GetItem(), which returns a borrowed reference. What must you do before storing this reference for later use?Py_INCREF() to take ownership, and then Py_DECREF() when you are finished. Calling Py_DECREF() on a borrowed reference without first incrementing it will corrupt the reference count and eventually cause a crash.How Is PEP 7 Enforced?
Unlike PEP 8, which has a rich ecosystem of automated linters and formatters like pycodestyle, flake8, and ruff, PEP 7 has no official automated enforcement tool. There is no pep7check or cpython-lint that you can run against your C code and get a pass/fail result. This is a meaningful gap, and it has practical consequences for contributors.
In practice, PEP 7 compliance is enforced through three mechanisms. The first and primary one is human code review. Every pull request to CPython is reviewed by at least one core developer who checks for style violations alongside correctness and performance. The second is CPython's CI infrastructure, which enforces a zero-warning policy across GCC, Clang, and MSVC. While this does not catch style issues like brace placement or naming convention violations, it does catch many of the problems PEP 7 is designed to prevent, such as implicit conversions, unused variables, and missing prototypes. The third is community familiarity: contributors who have read PEP 7 tend to produce conforming code, and the surrounding codebase itself serves as a reference for the expected style.
There have been community efforts to create a clang-format configuration file that approximates PEP 7's rules. Several such configurations exist as GitHub gists and forum posts, and the topic of auto-formatting CPython has been discussed among core developers. However, no official .clang-format file has been adopted into the CPython repository as of this writing. The challenge is that PEP 7's conventions -- particularly the return-type-on-its-own-line function definition style and the else-on-its-own-line brace placement -- do not map cleanly onto any of clang-format's built-in style presets.
If you are writing a C extension that is not part of CPython itself, PEP 7 compliance is a recommendation, not a requirement. But adopting its conventions will make your code immediately familiar to anyone in the CPython ecosystem, and it removes an entire category of style decisions you would otherwise have to make yourself.
The _Py Prefix and API Stability Boundaries
The naming conventions in PEP 7 do more than organize code visually. They encode a stability contract. Functions beginning with Py or Py_ are part of the public C API and are subject to backward compatibility guarantees. Functions beginning with _Py are internal to CPython. They are visible to the linker -- you can call them from extension code -- but they carry no stability promise whatsoever. They can change signature, change behavior, or disappear entirely between minor Python releases. see Naming Conventions
This distinction has become increasingly important in recent years as CPython has undergone significant internal changes. The introduction of the free-threaded build (PEP 703), the evolution of the stable ABI (PEP 384), and the ongoing evaluation of the public C API (PEP 733) have all sharpened the boundary between public and internal symbols. Extension authors who depend on _Py-prefixed functions risk breakage on every Python upgrade, and the CPython core team has been actively working to reduce the amount of internal detail exposed through the API headers.
/* Public API: stable across Python versions */
PyObject *PyList_GetItem(PyObject *list, Py_ssize_t index);
/* Internal API: may change or vanish without notice */
void _PyObject_Dump(PyObject *op);
/* Stable ABI (PEP 384): guaranteed across all 3.x versions
when compiled with Py_LIMITED_API defined */
PyObject *PyObject_GetAttr(PyObject *v, PyObject *name);
For extension authors, the practical rule is straightforward: never use _Py-prefixed symbols in code that needs to work across multiple Python versions. If you find yourself reaching for an internal function, it usually means there is either a public API alternative you have not found yet, or a gap in the public API that would benefit from a feature request on the CPython issue tracker. The naming convention in PEP 7 makes this boundary visible at a glance, which is exactly the kind of practical design decision that justifies having a style guide in the first place.
PEP 7 vs PEP 8: The Relationship Between Them
PEP 7 itself describes its relationship to PEP 8 as that of companion documents. PEP 8 is the style guide for Python code; PEP 7 is the style guide for the C code that implements Python. The two share a common philosophy, but they differ significantly in their specifics because C and Python are fundamentally different languages with different idioms.
Both PEPs share the 79-character line length limit. Both use 4-space indentation. Both emphasize readability over cleverness. Both acknowledge that rules exist to be broken when a valid reason applies. And both were authored at least in part by Guido van Rossum, which gives them a consistent philosophy: code is read far more often than it is written, so readability is the primary design goal.
The differences reflect the realities of C. PEP 7 requires explicit brace placement rules that Python does not need because Python uses indentation for block structure. PEP 7 requires explicit namespace management via naming prefixes because C has no module system. PEP 7 addresses macro hygiene, which has no Python equivalent. And PEP 7 must address C standard compatibility across platforms, a concern completely absent from PEP 8.
"Please see the companion informational PEP describing style guidelines for Python code." — PEP 7, Introduction, referring to PEP 8
If you work across both Python and C extension code, it is worth internalizing both style guides. The mental shift between them is modest because they share a common foundation, but the C-specific details in PEP 7 are precise enough that you will want to have the document at hand when writing your first CPython contribution or non-trivial C extension.
A frequent mistake when transitioning from Python to C extension writing is assuming PEP 8 applies to C code. It does not. The brace placement rules, the return statement rules, the naming conventions, and the macro rules are all PEP 7 specific. Always consult peps.python.org/pep-0007/ when writing C for CPython or CPython-adjacent projects.
Key Takeaways
- PEP 7 is a living document: Written in 2001 and updated as recently as August 2025, it evolves with CPython itself. Always check the current version at peps.python.org/pep-0007/ before making contributions to CPython.
- C standard version depends on Python version: Python 3.11+ targets C11 without optional features; Python 3.6 to 3.10 used C89 plus selected C99 additions; older versions used pure C89. Always match your code to the target branch's standard.
- Naming prefixes are a namespace and stability system: The
Py,Py_,_Py, and type-specific prefixes likePyList_are not decoration; they manage CPython's public API namespace and encode stability guarantees. Never depend on_Py-prefixed symbols in extension code that must survive across Python versions. - Braces are required in new code: Even where C allows them to be omitted, all new CPython C code must include braces around every control structure block. Do not add braces to existing unbraced code you are not otherwise modifying.
- Use PyDoc_STR and PyDoc_STRVAR: Raw string literals for docstrings bypass the
--without-doc-stringsbuild option and are not acceptable in CPython. Always wrap docstrings in the appropriate macro. - Static inline over macros: In new code, prefer
static inlinefunctions to macros wherever possible for type safety, debuggability, and to avoid double-evaluation bugs. - Centralize error handling with goto: Initialize all
PyObject *locals toNULL, usegoto errorto jump to a single cleanup block, and release all owned references withPy_XDECREF. This pattern is not optional; it is how CPython manages resource safety in a language without exceptions. - Reference counting is a style concern: Correct reference counting depends on clean, consistent code structure. The PEP 7 conventions around variable initialization, centralized cleanup, and explicit ownership tracking exist in large part to make reference counting bugs visible and preventable.
- Enforcement is human, not automated: No official linter enforces PEP 7. Compliance is maintained through code review, CI warning policies, and contributor familiarity with the codebase. Read the PEP and read existing CPython source before submitting patches.
- PEP 7 and PEP 8 are companions, not interchangeable: They share a philosophy but have different rules appropriate to their respective languages. Apply each only to the language it governs.
PEP 7 is a compact document, but the conventions it establishes have shaped millions of lines of C code across several decades of Python development. Reading it carefully is one of the most efficient ways to build an accurate mental model of how CPython is maintained and how to participate in that work. The source of truth is always the PEP itself, available at peps.python.org/pep-0007/, with version history tracked at the CPython PEPs repository on GitHub.