Skip to content

Modernization notes: Py_TPFLAGS_IMMUTABLETYPE, global state vs subinterpreters, richcompare convention #231

Description

@devdanzin

These are not bugs — they are design-level observations from a code review that may be worth considering for future development. No action is required.


1. Py_TPFLAGS_IMMUTABLETYPE not set on any type

All 6 types are created with PyType_FromSpec but none set Py_TPFLAGS_IMMUTABLETYPE (available since Python 3.10, which is the minimum version). This flag marks a type as immutable, preventing monkey-patching from Python (Type.new_attr = ...). For constraint solver types, immutability is the expected semantic.

Setting it would:

  • Prevent accidental type mutation from user code
  • Enable interpreter-level optimizations for immutable types
  • Better express the intent that these types are not designed to be modified at runtime

The tradeoff: any code that monkey-patches these types (unlikely for a constraint solver) would break.

2. Global state limits subinterpreter support despite multi-phase init

kiwisolver already uses multi-phase init (Py_mod_exec slot) — the modern pattern. However, all 12 PyObject* pointers (6 types + 6 exceptions) are stored in C globals rather than per-module state (m_size = 0). This means Py_mod_exec running in a second subinterpreter overwrites the globals from the first, creating dangling pointers when the first interpreter is destroyed.

Full subinterpreter support would require:

  1. Moving all 12 globals into a module state struct (m_size = sizeof(...))
  2. Using PyType_FromModuleAndSpec instead of PyType_FromSpec (to enable PyType_GetModuleState from type methods)
  3. Adding m_traverse/m_clear/m_free to manage the state

This is a MEDIUM-difficulty migration because kiwisolver already has the hardest parts done (multi-phase init, heap types). The main challenge is threading module state access through the symbolics.h templates, which currently access TypeObject globals for type checking and object construction.

Whether this is worth doing depends on whether subinterpreter support is a goal for kiwisolver's users (e.g., matplotlib in subinterpreter-based parallel rendering).

3. richcompare raises TypeError instead of returning Py_NotImplemented

Variable_richcompare, Term_richcompare, and Expression_richcompare raise TypeError directly for unsupported comparison operations (<, !=, >) instead of returning Py_NotImplemented. The Python data model convention is to return Py_NotImplemented to give the other operand's type a chance to handle the comparison via reflected methods.

However, for kiwisolver's domain this is arguably the right design:

  • ==, <=, >= create constraints, not boolean results
  • <, >, != have no mathematical meaning for constraint variables
  • Raising TypeError immediately gives a clear error message rather than falling through to a confusing default comparison

This is a deliberate design choice, not an oversight. Noted for documentation purposes.


These observations come from a systematic review using cext-review-toolkit. The full report covers all 10 analysis dimensions and is available on request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions