> On Feb 7, 2026, at 13:56, [email protected] wrote: > > Hi All, > I've been a happy GnuCash user for 13 years and am recently finding > more time and interest to take on some more advanced use cases and > also contribute to the GnuCash project. (and more bandwidth thanks to > AI assisted coding) > > My projects all involve use of the official API python bindings. So > you'll see some bug reports, PR, and other chatter from me about that. > > Here's a matter that's within my skillet to fix, but the best approach > is debatable given one route involves breaking changes to the existing > python bindings. So I wanted to seek other's opinions. > > Background > ----------------------------- > > The Python bindings use a two-layer architecture: SWIG auto-generates > a low-level C API (gnucash_core_c), and gnucash_core.py wraps selected > methods so they return proper Python objects (GncPrice, GncCommodity, > etc.) instead of raw SWIG pointers. This wrapping is done via > methods_return_instance() dicts that map method names to their return > types. > > The problem is that these dicts are incomplete. Many methods that > return pointers to GnuCash objects are exposed on the Python classes > (via add_methods_with_prefix) but never registered for return-type > wrapping. They silently return raw SwigPyObject pointers that have no > Python methods and can only be used via gnucash_core_c C-level > function calls. > > This is confusing because the methods *appear* to work -- they're > callable and return non-None values -- but the return values are > unusable through the documented Python API. There's no error, no > warning, and no documentation indicating which methods are wrapped and > which aren't. The only way to discover the problem is to inspect > type() on the return value. > > I've done a systematic audit of all classes in gnucash_core.py and > gnucash_business.py, cross-referencing the C functions exposed by SWIG > against the methods_return_instance dicts, and empirically verified > each finding. Below are the results. > > > Affected Classes and Methods > ----------------------------- > > 1. GncPrice -- no wrapping dict exists at all > ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ > > GncPrice is the worst case. bindings/python/gnucash_core.py line 741 > calls add_methods_with_prefix('gnc_price_'), which exposes all > gnc_price_* functions as methods. But there is no > methods_return_instance call for GncPrice anywhere in that file -- not > a single method has its return type wrapped. > > Method Currently returns Should return > ---------------- -------------------------------- ---------------- > get_commodity() SwigPyObject (raw gnc_commodity *) GncCommodity > get_currency() SwigPyObject (raw gnc_commodity *) GncCommodity > clone(book) SwigPyObject (raw GNCPrice *) GncPrice > get_value() _gnc_numeric (SWIG proxy) GncNumeric > > get_value() is a partial case -- the _gnc_numeric SWIG proxy is usable > via GncNumeric(instance=val), but this is inconsistent with Split and > Transaction where equivalent methods (GetAmount, GetValue, GetBalance, > etc.) are all wrapped to return GncNumeric directly. > > Suggested fix: > > GncPrice.add_methods_with_prefix('gnc_price_') > > gnc_price_dict = { > 'get_commodity': GncCommodity, > 'get_currency': GncCommodity, > 'clone': GncPrice, > 'get_value': GncNumeric, > } > methods_return_instance(GncPrice, gnc_price_dict) > > > 2. GncPriceDB -- PriceDB_dict incomplete > ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ > > The existing PriceDB_dict wraps lookup_latest, > lookup_nearest_in_time64, lookup_nearest_before_t64, and some > convert_balance methods. But several methods that return the same > types are missing. > > Missing single-value wrappers (should return GncPrice): > > Method Currently returns > Should return > ---------------------------------------- ------------------------ > ----------- > nth_price(commodity, n) SwigPyObject (GNCPrice *) GncPrice > lookup_day_t64(commodity, currency, date) SwigPyObject (GNCPrice *) GncPrice > > Missing single-value wrapper (should return GncNumeric): > > Method Currently returns > Should return > ----------------------------------------------- -------------------- > ----------- > convert_balance_nearest_before_price_t64(...) _gnc_numeric (raw) > GncNumeric > > Its siblings convert_balance_latest_price and > convert_balance_nearest_price_t64 are correctly wrapped. > > Missing list wrappers (should return list of GncPrice): > > Method Currently returns > Should return > -------------------------------------------------- > --------------------- ---------------- > lookup_latest_any_currency(commodity) > list[SwigPyObject] list[GncPrice] > lookup_nearest_before_any_currency_t64(comm, date) > list[SwigPyObject] list[GncPrice] > lookup_nearest_in_time_any_currency_t64(comm, date) > list[SwigPyObject] list[GncPrice] > > Note: get_latest_price, get_nearest_price, and get_nearest_before_price > are NOT bugs -- their C functions return gnc_numeric directly (the > price value, not a GNCPrice *), so the raw _gnc_numeric return is the > correct C type. They should arguably be wrapped to GncNumeric for > consistency, but that's a lower priority. > > Suggested fix: > > PriceDB_dict = { > 'lookup_latest': GncPrice, > 'lookup_nearest_in_time64': GncPrice, > 'lookup_nearest_before_t64': GncPrice, > 'nth_price': GncPrice, > 'lookup_day_t64': GncPrice, > 'convert_balance_latest_price': GncNumeric, > 'convert_balance_nearest_price_t64': GncNumeric, > 'convert_balance_nearest_before_price_t64': GncNumeric, > 'get_latest_price': GncNumeric, > 'get_nearest_price': GncNumeric, > 'get_nearest_before_price': GncNumeric, > } > methods_return_instance(GncPriceDB, PriceDB_dict) > > methods_return_instance_lists(GncPriceDB, { > 'get_prices': GncPrice, # already done > 'lookup_latest_any_currency': GncPrice, # new > 'lookup_nearest_before_any_currency_t64': GncPrice, # new > 'lookup_nearest_in_time_any_currency_t64': GncPrice, # new > }) > > > 3. GncCommodity -- two missing wrappers > ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ > > GncCommodity.clone is correctly wrapped (gnucash_core.py line 979), > but two other methods that return object pointers are not. > > Method Currently returns Should return > ------------------ ----------------------------------------- > ---------------------- > obtain_twin(book) SwigPyObject (raw gnc_commodity *) GncCommodity > get_namespace_ds() SwigPyObject (raw gnc_commodity_namespace *) > GncCommodityNamespace > > Additionally, get_quote_source() and get_default_quote_source() return > raw gnc_quote_source * pointers. However, gnc_quote_source has no > Python wrapper class, so these are a deeper design gap rather than a > simple omission -- there's nothing to wrap *to*. Currently the only > way to use them is via gnucash_core_c.gnc_quote_source_get_internal_name(ptr) > etc. A proper fix would require creating a new GncQuoteSource wrapper > class. > > > 4. Account -- one missing wrapper > ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ > > Method Currently returns Should return > ------------------------ -------------------------------- ------------ > get_currency_or_parent() SwigPyObject (raw gnc_commodity *) GncCommodity > > The existing account_dict wraps GetCommodity -> GncCommodity but > misses get_currency_or_parent, which returns the same C type. > > > 5. GncLot -- two missing wrappers > ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ > > Method Currently returns Should return > ---------------------- --------------------- --------------- > get_balance_before(sp) raw _gnc_numeric GncNumeric > get_split_list() list[SwigPyObject] list[Split] > > The existing gnclot_dict wraps get_balance -> GncNumeric but misses > get_balance_before. And get_split_list needs > method_function_returns_instance_list like Account.GetSplitList and > Transaction.GetSplitList already have. > > > The Breaking Change Problem > --------------------------- > > Every fix listed above changes a method's return type from a raw SWIG > pointer to a wrapped Python object. These are breaking changes: any > existing code that passes these return values to gnucash_core_c > C-level functions will break, because those functions expect the raw > SWIG pointer, not a wrapper. > > For example, current workaround code looks like: > > from gnucash import gnucash_core_c as gc > > raw_price = pricedb.nth_price(commodity, 0) > gc.gnc_price_get_currency(raw_price) # works today > gc.gnc_price_get_time64(raw_price) > > After wrapping nth_price -> GncPrice: > > price = pricedb.nth_price(commodity, 0) # now returns GncPrice > gc.gnc_price_get_currency(price) # BREAKS > price.get_currency() # new correct usage > > The workaround-after-the-fix is to use .instance to extract the raw > pointer: > > gc.gnc_price_get_currency(price.instance) # works > > How many users are affected? > > Anyone using these methods today has already discovered the raw-pointer > problem through trial and error and written gnucash_core_c workarounds. > These workarounds are undocumented and fragile. The "break" moves users > from an undocumented workaround to the intended API. > > That said, the Python bindings have been in this state for years, and > scripts using the C-level workarounds do exist in the wild (Stack > Overflow answers, wiki examples, personal scripts). > > > Possible Approaches > ------------------- > > I'd like the developers' input on how to handle this. Some options: > > Option A: Fix everything, accept the break > > Add all missing entries to the methods_return_instance dicts. > Document the change in release notes. This is the cleanest long-term > outcome but breaks existing workaround code silently (no error -- > just wrong types passed to C functions, likely causing segfaults or > TypeError). > > Option B: Fix everything, add a compatibility shim > > Modify method_function_returns_instance (in function_class.py) so > that wrapped objects are transparently accepted by gnucash_core_c > functions. This could be done by making the wrapper classes implement > __swig_convert__ or by patching process_list_convert_to_instance to > unwrap at call boundaries. This would make both old and new code > work, but adds complexity to the wrapping layer. > > Option C: Fix only the most impactful methods, leave the rest > > Prioritize the methods most likely to be encountered by users: > - GncPrice.get_commodity(), .get_currency() > - GncPriceDB.nth_price() > - Account.get_currency_or_parent() > > Leave edge cases like GncLot.get_balance_before() and > GncCommodity.obtain_twin() for later. > > Option D: Deprecation warnings first, fix later > > Add runtime deprecation warnings when unwrapped methods are called, > pointing users to the upcoming change. Fix the return types in the > next major release. > > --- > > My preference is Option A with clear release notes because the > compatibility shim may be too complex. The current state is a usability > trap -- methods look like they work but return unusable objects -- and > fixing it benefits all future users even if it inconveniences the few > who have written workarounds. > > I'm happy to submit patches for whichever approach the project prefers. > > > Appendix: Methodology > --------------------- > > All findings were verified empirically on GnuCash 5.14 built from > source (Python 3.11, Debian Bookworm, -DWITH_PYTHON=ON > -DWITH_GNUCASH=OFF). Each method was called against a test GnuCash > SQLite file containing accounts, commodities, and prices. Return types > were checked with type() and compared against the C function signatures > in gnc-pricedb.h, gnc-commodity.h, Account.h, and gnc-lot.h. > > The full C API surface was enumerated via dir(gnucash_core_c) and > cross-referenced against the methods_return_instance dicts in > gnucash_core.py (lines 769-776, 960-974, 984-992, 1011-1020, > 1029-1044, 1056-1074, 1085-1114) and gnucash_business.py. >
Thanks for being willing to take this on. Unfortunately we have no idea how many python bindings users there are nor how sophisticated any of them are about working around the bindings’ limitations. Our general policy is that we wouldn’t remove API without at least a release or two worth of notice via deprecation warnings, so if we got deprecations in the upcoming 5.15 release we’d wait until the end of the year to remove the API. It’s also not ideal to deprecate API before the replacement is available making your option B the best choice. The breakage problem seems to me to be what SWIG typemaps are for, and it looks to me like the python bindings don’t make much use of typemaps. Did you consider that and if not can you? Regards, John Ralls _______________________________________________ gnucash-devel mailing list [email protected] https://lists.gnucash.org/mailman/listinfo/gnucash-devel
