Skip to content

Support VT_ERROR variant type. - #965

Merged
junkmd merged 9 commits into
enthought:mainfrom
junkmd:support_variant_error
Oct 12, 2026
Merged

junkmd merged 9 commits into
enthought:mainfrom
junkmd:support_variant_error

Conversation

@junkmd

@junkmd junkmd commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR fixes how VT_ERROR values are returned from a VARIANT.

Closes #821
Related to #325

What changed

A VT_ERROR value contains an error code in its scode field.

The old code did not handle VT_ERROR in _get_value(). Therefore, reading an Excel error cell raised:

>>> from comtypes import hresult
>>> from comtypes.client import CreateObject, GetModule
>>> GetModule(("{00020813-0000-0000-C000-000000000046}",))
<module 'comtypes.gen.Excel' from ...>
>>> xl = CreateObject("Excel.Application")
>>> xl.Visible = True
>>> wb = xl.Workbooks.Add()
>>> ws = wb.Worksheets(1) 
>>> ws
<POINTER(_Worksheet) ptr=... at ...>
>>> ws.Name
'Sheet1'
>>> rng = ws.Range("A1") 
>>> rng.Formula = "=1/0"
>>> print(rng.Value())
Traceback (most recent call last):
  ...
NotImplementedError: typecode 10 = 0xa)

This PR now returns the original VARIANT object for VT_ERROR.

For example:

>>> from comtypes import hresult
>>> from comtypes.client import CreateObject, GetModule
>>> GetModule(("{00020813-0000-0000-C000-000000000046}",))
<module 'comtypes.gen.Excel' from ...>
>>> xl = CreateObject("Excel.Application")
>>> xl.Visible = True
>>> wb = xl.Workbooks.Add()
>>> ws = wb.Worksheets(1) 
>>> ws
<POINTER(_Worksheet) ptr=... at ...>
>>> ws.Name
'Sheet1'
>>> rng = ws.Range("A1") 
>>> rng.Formula = "=1/0"
>>> print(rng.Value())
VARIANT(vt=0xa, -2146826281)
>>> # The result is still a `VARIANT` with `VT_ERROR`.
>>> print(xl.WorksheetFunction.IsError(rng.Value()))
True
>>> rng.Value()._.scode
-2146826281

Why _get_value() does not return self._.scode

self._.scode contains the error code, but it does not contain the full meaning of the value.

If _get_value() returns only self._.scode, the div0err_rng.Value() result becomes -2146826281, a normal Python integer.

The caller can no longer tell whether this integer is a normal integer or an error value from a VT_ERROR VARIANT.

Returning self keeps both parts:

  • vt=VT_ERROR
  • scode=-2146826281

This is important because COM users and Excel need to know that the value is an error.
It also allows WorksheetFunction.IsError() to return True.

self._.scode is still used when the code needs to inspect the actual error code. It is not used as the return value because returning it would remove the VARIANT type information.

This commit log records the history of an attempt to make the value property return error codes, which caused compatibility issues with Excel and led to the current design.
Please refer to that log for technical implementation details.

Excel example

For the =1/0 formula, Excel returns a VT_ERROR value.
The error code is -2146826281.
As an unsigned hexadecimal value, this is:

from comtypes import hresult

hresult.int_to_signed32bithex(-2146826281)
# '0x800A07D7'

This is the COM error code for Excel #DIV/0!.

Excel builds this HRESULT from the Excel error number.
The Excel error number for #DIV/0! is 2007, or 0x07D7 in hexadecimal.
https://learn.microsoft.com/en-us/office/vba/api/excel.xlcverror
Excel adds the Excel facility value 0x800A0000, so the result is 0x800A07D7.

>>> hresult.int_to_signed32bithex(-2146826281)
'0x800A07D7'
>>> hresult.signed32bithex_to_int('0x07D7')
2007

Impact on existing users

Code that catches NotImplementedError from rng.Value() may need to change.

try:
    val = rng.Value()
except NotImplementedError:
    ...

The call no longer raises that error for VT_ERROR.
The caller should check the returned VARIANT instead:

val = rng.Value()
if isinstance(val, VARIANT) and val.vt == VT_ERROR:
    ...

Alternatively, use a COM server-side utility like IsError:

if rng.Application.WorksheetFunction.IsError(rng):
    ...

This keeps the error type and the error code available to the caller.

Test without Excel

The Excel test checks the real Excel behavior, but Excel cannot be used in the CI pipeline.
To address this, the test suite also uses the native C++ COM test server in source/CppTestSrv.
It returns VT_ERROR values and reads their scode values through a real COM call.

The CI job builds and registers this local server with MSVC before running the tests.
This is a native COM test double for the Excel part of this change.
It runs in CI without installing or starting Excel, so the test checks the VARIANT behavior on its own.

Tests

The tests check that:

  • VT_ERROR values can be read without raising NotImplementedError
  • the returned value is a VARIANT
  • the returned VARIANT keeps VT_ERROR
  • the scode value is preserved
  • Excel error cells return an error VARIANT
  • WorksheetFunction.IsError() returns True
  • the native C++ COM test server checks the same behavior without Excel

…shalling tests.

- Introduce a C++ native COM test double for creating and reading `VT_ERROR`
  `VARIANT`s.
- Ensure test double validates error variant marshalling across Python and
  COM boundaries.
- Added early return for VT_ERROR in `tagVARIANT.__ctypes_from_outparam__`.
- So `scode` is inline value, no extra free needed by caller.
- Added test suite `test_variant_error` covering error variant behavior.
- Added `scode` field to `tagVARIANT` (`U_VARIANT2`) to expose error codes
  directly.
- The addition of `scode` does not change the size or memory layout of
  `tagVARIANT` because it occupies the existing union slot, preserving
  binary layout.
- No backward‑compatible breaking changes; existing code continues to work
  unchanged.
The relevant `VARIANT` layout uses a `U_VARIANT2(Union)` in which
`("VT_I4", c_long)` and `("scode", SCODE)` share the same 32-bit storage.
In the cached diff, the code first sets `v.vt = VT_ERROR` to declare that
this slot is being used as a `VT_ERROR` variant, and then writes `0x80020004`
into the underlying field.

This value is `0x80020004` in the raw bit pattern, which is the HRESULT bit
pattern for `DISP_E_PARAMNOTFOUND`. In Python, that pattern is represented as
`2147549188`, but when read as a signed 32-bit integer it becomes
`-2147352572`, which is exactly `hresult.DISP_E_PARAMNOTFOUND`.

The key point is that the same 32-bit storage is interpreted differently
depending on the active `vt` tag: when the tag is `VT_ERROR`, the value is
read back as `scode`. Therefore, the real behavior is:

- `v.vt = VT_ERROR` declares that this `VARIANT` is an error value.
- `v._.VT_I4 = 0x80020004` writes the HRESULT bit pattern into the shared
  union storage.
- `v.value` reads that same storage as `scode`.
- `v.value == hresult.DISP_E_PARAMNOTFOUND` is therefore true.

This means the existing code was not wrong to assign the value to
`v._.VT_I4`. The issue discussion was based on a misunderstanding of how
`VARIANT` stores `VT_ERROR` values, not on a real defect in the code itself.
Make `VARIANT._get_value()` return the original `VARIANT` for `VT_ERROR`.
Do not turn its `scode` into a plain integer.
Excel returns `=1/0` as an error cell with `VT_ERROR` and an HRESULT.
If `Range.Value()` returns only `-2146826281`, users cannot tell an error
code from a real integer.
Now `Range.Value()` returns `VARIANT(vt=0xa, -2146826281)`, and
`WorksheetFunction.IsError()` returns `True`.
The negative number is the signed form of COM HRESULT `0x800A07D7`.
It is the error value for Excel `#DIV/0!`.
This fixes the old `NotImplementedError` while keeping the error type.

Remove the old `VT_ERROR` branch in `__ctypes_from_outparam__()`.
`scode` is an inline value, not a pointer, so returning the `VARIANT` keeps
the error data. No special resource cleanup is needed.
Update the `VARIANT.missing` checks to use `._.scode`, not `.value`.
Also update the regression tests to check the error value and the object.
Add an Excel test for a cell with the formula `=1/0`.
Excel should return a `VARIANT` with `VT_ERROR`, not a plain integer.
This test checks that `Range.Value()` keeps the error type.
It also checks that its `scode` is HRESULT `0x800A07D7`.
That HRESULT is the signed error value for Excel `#DIV/0!`.
The test protects the new behavior from future changes.
@codecov-commenter

codecov-commenter commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.04478% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.21%. Comparing base (939341c) to head (121f60b).

Files with missing lines Patch % Lines
comtypes/test/test_excel.py 20.00% 4 Missing ⚠️
comtypes/test/test_variant_error.py 94.28% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #965   +/-   ##
=======================================
  Coverage   89.21%   89.21%           
=======================================
  Files         142      143    +1     
  Lines       13870    13933   +63     
=======================================
+ Hits        12374    12431   +57     
- Misses       1496     1502    +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@junkmd junkmd linked an issue Oct 12, 2026 that may be closed by this pull request
@junkmd junkmd added this to the 1.5.0 / Support Python 3.15 milestone Oct 12, 2026
Ensure unsupported `VARIANT` types retain their explicit `NotImplementedError` behavior.
@junkmd
junkmd force-pushed the support_variant_error branch from 5865121 to 121f60b Compare October 12, 2026 03:40
@junkmd junkmd added enhancement New feature or request tests enhance or fix tests 3rd-party issue related to specific windows stuffs or python packages labels Oct 12, 2026
@junkmd junkmd changed the title Support Error Variant. Support VT_ERROR variant type. Oct 12, 2026
@junkmd
junkmd merged commit 8f4137c into enthought:main Oct 12, 2026
148 of 151 checks passed
@junkmd
junkmd deleted the support_variant_error branch October 12, 2026 04:37
@junkmd junkmd linked an issue Oct 12, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3rd-party issue related to specific windows stuffs or python packages enhancement New feature or request tests enhance or fix tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support VT_ERROR variant type and optional parameters Replace the integer literal assigned to VARIANT.missing with a constant.

2 participants