Architecture¶
Overview¶
Python public API: attributedict.AttributeDict (thin wrapper)
↓
C extension: attributedict._attributedict (module init)
↓
C type: AttributeDict_Type (PyTypeObject)
- inherits from dict (PyDict_Type) -> isinstance(d, dict) is True
- custom tp_getattro / tp_setattro -> keys-win attribute access
- custom tp_new / tp_init -> recursive nested conversion
- GC slots (tp_traverse / tp_clear) -> cycle safety
C API strategy¶
Decision: Limited API / Stable ABI (abi3), Py_LIMITED_API target 3.9,
wheel tag cp39-abi3. This yields one wheel per platform across
CPython 3.9–3.14.
Decision: C subclass of dict rather than composition or a
reimplementation. The dict base provides the mapping protocol, views,
iteration, equality, pickling, and copy behavior for free; the type only
overrides attribute access and construction.
Mapping-protocol approach¶
Because AttributeDict_Type subclasses PyDict_Type:
mp_length,mp_subscript,mp_ass_subscript,sq_containsare inherited from dict — no overrides needed.tp_richcompareis inherited from dict — equality is dict semantics.tp_hashis inherited from dict — unhashable.- dict methods (
get,setdefault,update,pop,popitem,clear,copy,keys,items,values,fromkeys) are inherited.copyandfromkeysare overridden to returnAttributeDict.copyreturnsAttributeDictviatp_copy-equivalent (dict'scopyreturns the same type for subclasses viaPyDict_Copy— verified at implementation time in ).
Overridden slots¶
| Slot | Purpose | Notes |
|---|---|---|
tp_getattro |
keys-win attribute read | see resolution order below |
tp_setattro |
attribute write/delete → mapping ops | |
tp_new / tp_init |
construction forms + recursive conversion | cycle-safe |
tp_repr |
AttributeDict({...}) |
|
tp_traverse / tp_clear |
GC participation | portable pattern below |
tp_dealloc |
delegate to dict base | no double-free |
GC strategy for a dict subclass under the Limited API¶
PyObject_VisitManagedDict/PyObject_ClearManagedDict are not public
Limited-API functions in 3.9–3.13. The portable pattern for a C type that
subclasses dict and holds references in the dict:
static int
AttributeDict_traverse(AttributeDict *self, visitproc visit, void *arg)
{
PyObject *items = PyDict_Items((PyObject *)self);
if (items == NULL) { return -1; }
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(items); i++) {
Py_VISIT(PyList_GET_ITEM(items, i)); /* visits key and value tuples */
}
Py_DECREF(items);
return 0;
}
tp_clear clears the dict contents with PyDict_Clear (leaving an empty
AttributeDict). This is safe for cycles and correct across 3.9–3.14.
(At implementation time, verify whether the items-tuple
traversal fully covers key+value references; if not, iterate pairs directly.)
Attribute resolution order (tp_getattro) — keys win¶
- If the attribute name is a
strthat is a valid identifier (PyUnicode_IsIdentifier, available 3.9+) and a key exists (PyDict_GetItemWithError, available 3.9+): return the key's value. - Otherwise fall back to
PyObject_GenericGetAttr. - If that raises
AttributeError, propagate it.
This implements the resolution order: real type attributes win on the attribute path, and non-identifier keys are not reachable via attribute syntax.
Attribute set/delete (tp_setattro)¶
d.name = v→PyDict_SetItem(self, name, v).del d.name→PyDict_DelItem(self, name); onKeyErrorre-raise asAttributeError(deviation documented in spec 08).- Names that are not identifiers:
PyDict_SetItemstill works for mapping syntax; attribute syntax simply never resolves them.
Verified Limited-API availability (CPython 3.13 headers, 3.9+ target)¶
| API | Status |
|---|---|
PyDict_Type (PyAPI_DATA) |
✅ public |
PyDict_GetItemWithError |
✅ public |
PyDict_SetItem / PyDict_DelItem / PyDict_Contains |
✅ public |
PyObject_GenericGetAttr / PyObject_GenericSetAttr |
✅ public |
PyObject_GenericGetDict |
✅ public |
PyUnicode_IsIdentifier |
✅ public |
Py_ReprEnter / Py_ReprLeave |
✅ public (repr recursion) |
Py_VISIT / Py_CLEAR macros |
✅ public |
Py_TPFLAGS_BASETYPE, Py_TPFLAGS_HAVE_GC |
✅ public |
PyObject_VisitManagedDict / PyObject_ClearManagedDict |
❌ not public in 3.13 (use portable pattern) |
PyObject_GC_Visit / PyObject_GC_Clear |
❌ not public (use Py_VISIT/Py_CLEAR) |
No required API is missing from the Limited API for 3.9+; the GC helpers use
the portable Py_VISIT/Py_CLEAR pattern instead.
C source layout¶
src/attributedict/
├── __init__.py # public re-export
├── py.typed
└── _attributedict.c # module init + type definition
If complexity grows, split into module.c, attributedict.c/.h,
conversion.c/.h, attributes.c/.h — only if each file has a coherent
responsibility ( maintainability).
References¶
- spec 05 (system architecture), spec 09 (packaging), spec 07 (memory), decisions ////.