CPython's SimpleNamespace Implementation in C
types.SimpleNamespace as the C type _PyNamespace_Type: the _PyNamespaceObject struct, its backing dict exposed as __dict__, tp_new/tp_init, generic attribute access, repr, equality, __reduce__, __replace__, GC, and the _PyNamespace_New helper traced through CPython 3.15.
Python’s types.SimpleNamespace looks like a tiny “bag of attributes”
class, but in CPython 3.15 it is a real C type, _PyNamespace_Type,
defined in Objects/namespaceobject.c. There is no separate attribute
storage: every attribute lives in an ordinary dict that is also the
object’s __dict__. This article traces that implementation.
The struct
The whole object is a PyObject_HEAD plus one pointer:
// Objects/namespaceobject.c
typedef struct {
PyObject_HEAD
PyObject *ns_dict;
} _PyNamespaceObject;That single ns_dict is the entire state. __dict__ is not a separate
property layered on top; it is ns_dict. This is exposed twice in the
type table: once as a read-only member and once as tp_dictoffset:
// Objects/namespaceobject.c
static PyMemberDef namespace_members[] = {
{"__dict__", _Py_T_OBJECT, offsetof(_PyNamespaceObject, ns_dict), Py_READONLY},
{NULL}
};// Objects/namespaceobject.c (in _PyNamespace_Type)
offsetof(_PyNamespaceObject, ns_dict), /* tp_dictoffset */Because tp_dictoffset points at ns_dict, CPython’s generic machinery
treats that slot as the instance dictionary. __dict__ access through
the member definition and through tp_dictoffset therefore return the
same object, which keeps obj.__dict__, vars(obj), attribute access,
and pickling all consistent.
The type table
_PyNamespace_Type is the C object behind types.SimpleNamespace. Its
important slots are:
// Objects/namespaceobject.c
PyTypeObject _PyNamespace_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"types.SimpleNamespace", /* tp_name */
sizeof(_PyNamespaceObject), /* tp_basicsize */
0, /* tp_itemsize */
namespace_dealloc, /* tp_dealloc */
...
namespace_repr, /* tp_repr */
...
PyObject_GenericGetAttr, /* tp_getattro */
PyObject_GenericSetAttr, /* tp_setattro */
...
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_BASETYPE, /* tp_flags */
...
namespace_traverse, /* tp_traverse */
namespace_clear, /* tp_clear */
namespace_richcompare, /* tp_richcompare */
...
namespace_methods, /* tp_methods */
namespace_members, /* tp_members */
...
offsetof(_PyNamespaceObject, ns_dict), /* tp_dictoffset */
namespace_init, /* tp_init */
PyType_GenericAlloc, /* tp_alloc */
namespace_new, /* tp_new */
PyObject_GC_Del, /* tp_free */
};Three choices matter. Py_TPFLAGS_BASETYPE is set, so
SimpleNamespace can be subclassed (the doc example
class AdvancedNamespace(types.SimpleNamespace) works). Py_TPFLAGS_HAVE_GC
is set because the instance holds references to arbitrary objects in its
dict. Attribute access is delegated to PyObject_GenericGetAttr and
PyObject_GenericSetAttr, which look things up in __dict__; there is no
custom tp_getattr/tp_setattr, so the dict is the single source of
truth.
The type is exported to Python from Modules/_typesmodule.c:
// Modules/_typesmodule.c
EXPORT_STATIC_TYPE("SimpleNamespace", _PyNamespace_Type);and, in a build without the C extension, Lib/types.py falls back to
SimpleNamespace = type(sys.implementation), since sys.implementation
is itself a SimpleNamespace.
Allocation and initialization
namespace_new allocates the object and immediately creates an empty dict:
// Objects/namespaceobject.c
static PyObject *
namespace_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *self;
assert(type != NULL && type->tp_alloc != NULL);
self = type->tp_alloc(type, 0);
if (self != NULL) {
_PyNamespaceObject *ns = (_PyNamespaceObject *)self;
ns->ns_dict = PyDict_New();
if (ns->ns_dict == NULL) {
Py_DECREF(ns);
return NULL;
}
}
return self;
}namespace_init implements the documented constructor signature
SimpleNamespace(mapping_or_iterable=(), /, **kwargs):
// Objects/namespaceobject.c
static int
namespace_init(PyObject *op, PyObject *args, PyObject *kwds)
{
_PyNamespaceObject *ns = _PyNamespace_CAST(op);
PyObject *arg = NULL;
if (!PyArg_UnpackTuple(args, _PyType_Name(Py_TYPE(ns)), 0, 1, &arg)) {
return -1;
}
if (arg != NULL) {
PyObject *dict;
if (PyDict_CheckExact(arg)) {
dict = Py_NewRef(arg);
}
else {
dict = PyObject_CallOneArg((PyObject *)&PyDict_Type, arg);
if (dict == NULL) {
return -1;
}
}
int err = (!PyArg_ValidateKeywordArguments(dict) ||
PyDict_Update(ns->ns_dict, dict) < 0);
Py_DECREF(dict);
if (err) {
return -1;
}
}
if (kwds == NULL) {
return 0;
}
if (!PyArg_ValidateKeywordArguments(kwds)) {
return -1;
}
return PyDict_Update(ns->ns_dict, kwds);
}A positional argument is either used directly (if it is exactly a dict)
or converted with dict(arg); every key must be a string, enforced by
PyArg_ValidateKeywordArguments. Then PyDict_Update merges it into
ns_dict, and keyword arguments are merged the same way. Because the
backing store is a normal dict, insertion order is preserved and drives
the repr order.
Repr
The repr walks the dict’s keys in insertion order and formats each as
key=value!r:
// Objects/namespaceobject.c
static PyObject *
namespace_repr(PyObject *ns)
{
...
keys = PyDict_Keys(d);
...
while ((key = PyIter_Next(keys_iter)) != NULL) {
if (PyUnicode_Check(key) && PyUnicode_GET_LENGTH(key) > 0) {
PyObject *value, *item;
int has_key = PyDict_GetItemRef(d, key, &value);
if (has_key == 1) {
item = PyUnicode_FromFormat("%U=%R", key, value);
Py_DECREF(value);
...
}
else if (has_key < 0) {
loop_error = 1;
}
}
...
}
...
repr = PyUnicode_FromFormat("%s(%S)", name, pairsrepr);
}The name is "namespace" for an exact _PyNamespace_Type instance and
Py_TYPE(ns)->tp_name for subclasses, so a subclass repr uses its own
class name. PyDict_GetItemRef (rather than the older
PyDict_GetItemWithError) is used so the borrowed reference is safely
converted to an owned one before formatting, which is what fixed a crash
in 3.15 when the dict mutated during repr.
Equality
Equality compares the backing dicts directly, but only for ==/!= and
only when both operands are namespace objects:
// Objects/namespaceobject.c
static PyObject *
namespace_richcompare(PyObject *self, PyObject *other, int op)
{
if (
(op == Py_EQ || op == Py_NE) &&
PyObject_TypeCheck(self, &_PyNamespace_Type) &&
PyObject_TypeCheck(other, &_PyNamespace_Type)
) {
return PyObject_RichCompare(((_PyNamespaceObject *)self)->ns_dict,
((_PyNamespaceObject *)other)->ns_dict, op);
}
Py_RETURN_NOTIMPLEMENTED;
}For any other comparison operator (or non-namespace operand) it returns
NotImplemented, letting Python fall back to identity comparison or raise
TypeError. The op == Py_EQ || op == Py_NE guard avoids passing an
ordering operator to dict.__eq__.
Pickling and copying
__reduce__ returns a three-tuple (type, (), ns_dict), which makes
unpickling call type() and then update its dict from the third element:
// Objects/namespaceobject.c
static PyObject *
namespace_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
{
_PyNamespaceObject *ns = (_PyNamespaceObject*)op;
PyObject *result, *args = PyTuple_New(0);
if (!args)
return NULL;
result = PyTuple_Pack(3, (PyObject *)Py_TYPE(ns), args, ns->ns_dict);
Py_DECREF(args);
return result;
}__replace__, used by copy.replace, allocates a new instance of the
same type, copies the old dict in, then applies the keyword overrides:
// Objects/namespaceobject.c
static PyObject *
namespace_replace(PyObject *self, PyObject *args, PyObject *kwargs)
{
if (!_PyArg_NoPositional("__replace__", args)) {
return NULL;
}
PyObject *result = PyObject_CallNoArgs((PyObject *)Py_TYPE(self));
if (!result) {
return NULL;
}
if (!_PyNamespace_Check(result)) {
PyErr_Format(PyExc_TypeError,
"expect %N type, but %T() returned '%T' object",
&_PyNamespace_Type, self, result);
Py_DECREF(result);
return NULL;
}
if (PyDict_Update(((_PyNamespaceObject*)result)->ns_dict,
((_PyNamespaceObject*)self)->ns_dict) < 0)
{
Py_DECREF(result);
return NULL;
}
if (kwargs) {
if (PyDict_Update(((_PyNamespaceObject*)result)->ns_dict, kwargs) < 0) {
Py_DECREF(result);
return NULL;
}
}
return result;
}The _PyNamespace_Check(result) guard matters because Py_TYPE(self)
could be a subclass whose __new__/__init__ returns something that is
not a namespace; without it the subsequent cast of result would be
undefined behavior. This guard was added in 3.15.
GC, clear, and dealloc
Because ns_dict can reference arbitrary objects (including cycles), the
type participates in the cyclic GC:
// Objects/namespaceobject.c
static int
namespace_traverse(PyObject *op, visitproc visit, void *arg)
{
_PyNamespaceObject *ns = _PyNamespace_CAST(op);
Py_VISIT(ns->ns_dict);
return 0;
}
static int
namespace_clear(PyObject *op)
{
_PyNamespaceObject *ns = _PyNamespace_CAST(op);
Py_CLEAR(ns->ns_dict);
return 0;
}
static void
namespace_dealloc(PyObject *op)
{
_PyNamespaceObject *ns = _PyNamespace_CAST(op);
PyObject_GC_UnTrack(ns);
Py_CLEAR(ns->ns_dict);
Py_TYPE(ns)->tp_free((PyObject *)ns);
}tp_free is PyObject_GC_Del, matching tp_alloc = PyType_GenericAlloc
for a GC-tracked type.
The internal helper
CPython itself creates SimpleNamespaces in many places (module specs,
sys.implementation, interpreter configs, instrumentation events, etc.)
through a small helper that skips __init__:
// Objects/namespaceobject.c
PyObject *
_PyNamespace_New(PyObject *kwds)
{
PyObject *ns = namespace_new(&_PyNamespace_Type, NULL, NULL);
if (ns == NULL)
return NULL;
if (kwds == NULL)
return ns;
if (PyDict_Update(((_PyNamespaceObject *)ns)->ns_dict, kwds) != 0) {
Py_DECREF(ns);
return NULL;
}
return (PyObject *)ns;
}It calls namespace_new directly (allocating and creating the empty dict)
and then merges kwds. Passing NULL yields an empty namespace. This is
the function exported by Include/internal/pycore_namespace.h and used by
import.c, sysmodule.c, timemodule.c, _interpretersmodule.c, and
others.
Summary
types.SimpleNamespace is deliberately minimal: a PyObject_HEAD plus a
dict. Attribute access, __dict__, GC, and instance dictionary handling
are all inherited from CPython’s generic object machinery, while the type
supplies allocation, initialization from a mapping/iterable plus kwargs,
repr, equality, pickling, and __replace__. Every attribute is just a
key in ns_dict, which is why SimpleNamespace behaves like a
mutable, attribute-accessible dict.