Repository navigation
Fix the tlbparser ctypes mapping for VT_I1. #969
Description
Summary
tools.tlbparser maps the COM type VT_I1 to ctypes.c_char.
This mapping has been used since the early days of the project.
However, VT_I1 means a signed 8-bit integer (see MSLearn). ctypes.c_char represents one byte of character data.
So, the current mapping does not match the meaning of the COM type.
#962 by @haoxuanjng-lang proposes mapping VT_I1 to ctypes.c_byte.
This change may help handle numeric data correctly and fix problems with NumPy support.
However, this mapping has been used for a long time.
Changing it may affect existing Python code.
We need to check both the need for this fix and its impact on users.
Proposed Changes
The generated types for VT_I1 will change:
- For
VT_I1,c_charwill becomec_byte. - For pointers to
VT_I1,STRINGwill becomePOINTER(c_byte).- Other types that generate
STRING, such asVT_LPSTR, are not affected.
- Other types that generate
This change may also affect arrays and other types that contain VT_I1, such as pointers, SAFEARRAYs, and C arrays.
Important
How this package marshals automation.VARIANT values is outside the scope of this issue. This issue is limited to the type library parser and code generator.
Problems
1. NumPy support
VT_I1 is a signed 8-bit integer. Its matching NumPy data type is numpy.int8.
However, when VT_I1 is mapped to ctypes.c_char, SAFEARRAY elements may be treated as character data. This causes problems when creating or converting NumPy arrays.
Negative values or array shapes also not work as expected.
Mapping VT_I1 to the correct signed integer type may fix these problems.
2. The relation between COM types and ctypes types
COM type information defines data types, such as method arguments and return values. comtypes reads this information with tlbparser and creates Python modules with codegenerator.
In these modules, ctypes types are used for COM method arguments and return values, as well as structure and union fields. These types must match the meaning of the data defined by COM type information.
VT_I1 means a signed 8-bit integer, so ctypes.c_byte matches its meaning. ctypes.c_char also uses one byte, but it is for character data. The two types do not have the same meaning.
Therefore, the mapping in tlbparser affects the types of method arguments and return values, as well as structure and union fields in generated modules.
We need the correct mapping so that these types match the COM type information.
3. Long-standing behavior and backward compatibility
The mapping from VT_I1 to ctypes.c_char has been used since the early days of the project.
Existing Python code may depend on this behavior.
For example, users may pass bytes to generated methods or expect return values to be bytes.
Some code may also depend on structure or union fields using c_char.
#962 improves the handling of numeric values. However, it also affects some existing calls.
Long-standing behavior is not always intended behavior. However, fixing a bug and ignoring its impact on existing users are two different things.
If the impact of the change is very small, or users can easily fix their calling code, this is a strong reason to make the change. However, if important libraries or common use cases are affected, and users cannot easily fix the problem, this may block the change.
We need to understand not only why the fix is needed, but also which code is affected, how users can fix the problem, and how much work is required.
Based on this information, we can decide how to change the mapping and how to handle backward compatibility.
Completion criteria
VT_I1is mapped to the correct ctypes type for a signed 8-bit integer.- Generated method arguments and return values, as well as structure and union fields, match the COM type information.
- NumPy SAFEARRAY support gives the expected values, signs, dtype, and shape.
- The impact on existing code and the work needed to fix it are clear.
- Important affected libraries and use cases have been checked, and a plan for backward compatibility has been decided.
- The required tests and documentation are ready.
Activity
junkmd commentedon Oct 17, 2026
@haoxuanjng-lang
I am also thinking about showing a warning when a type library uses
VT_I1during code generation (more exactly, when parsing the type library).This would help users check how your proposed changes in #962 may affect them.
Please compare the pros and cons and let me know what you think.
haoxuanjng-lang commentedon Oct 17, 2026
I regenerated the four installed Microsoft type libraries twice, with the legacy and proposed
tlbparsermappings in separate Python processes and separate empty generation directories. I inspected the generated declarations and constructed local records; this is not an end-to-end test of a speech engine, portable device, CLR object or MSVidCtl event source.SPPHRASERULE.Confidence,SPPHRASEPROPERTY.Confidence,SPPHRASEELEMENT.RequiredConfidence/ActualConfidencesapi.idldeclares signed-byte confidence andSP_LOW_CONFIDENCE = -1. In each freshly generated record, writing native0xffgivesb'\xff'with the old mapping and-1with the proposed one; assigning-1raisesTypeErrorbefore and works after. This supports the numeric fix. Code expecting bytes or usingord()must change; numeric comparison with the low-confidence constant becomes natural.cVal/pcVal, andtagCAC.pElemscValchanges from byte data to signed integer.pcValandpElemschange fromSTRINGtoPOINTER(c_byte). For a localCACwithcElems=3and data(-1, 0, 1), oldpElemsyields onlyb'\xff'because of the embedded NUL; new indexed access yields all three signed numbers. Callers must honorcElemsand retain the backing allocation. This concerns these generated records, not changes toautomation.VARIANTmarshaling.SByte.m_value,IConvertible.ToSByte,IFormatterConverter.ToSByteSByte.m_valuereads native0xffas bytes before and-1after. BothToSBytegenerated retval parameters change fromSTRINGtoPOINTER(c_byte), matching their documented signed numeric return. This is declaration-level evidence for the real interfaces; I have not invoked those CLR methods, so I am not claiming their runtime exception is fixed._IMSVidCtlEvents.Error.CancelDisplaymsvidctl.idldeclaresboolean*, a mutable flag rather than string content. Fresh generation changes its declaration fromSTRINGtoPOINTER(c_byte). This is a potentially important event-handler compatibility case: code treating a value as bytes, indexing it, or writing through it may need changes. I have not exercised the IDispatch event path, and its interaction with VARIANT marshaling needs separate validation.The matched generated record sizes and field offsets were equal across the two mappings on this Windows x64 installation. That supports layout compatibility for these records; it does not establish Python calling compatibility or the prevalence of affected applications. In particular, bytes and integer truth tests differ (
b'\0'versus0), so migrations must check logic as well as argument types. The native test fixture already demonstrates bytes inputs and writable character buffers becomingArgumentError; the revised tests assert those exact exceptions.Sources: Microsoft SAPI rule confidence, SAPI element confidence, PROPVARIANT and counted arrays, IConvertible.ToSByte, and IFormatterConverter.ToSByte; installed SDK
sapi.idlandmsvidctl.idl, and the regenerated libraries listed above.For the parsing-time warning suggested here, I reviewed a409eea:
-W error._uses_vt_i1does not followVT_USERDEFINED, so alias/reference cases deserve a check. I would prefer a warning in a transition release before changing the default rather than treating a simultaneous warning as a compatibility solution.These findings support the need for the signed mapping, but I cannot conclude that the real-world impact is small or that the MSVidCtl event path is safe. I am keeping #962 as a draft pending the compatibility/transition decision. I have not copied the warning prototype into the PR.
AI assistance: prepared and validated with OpenAI Codex.
junkmd commentedon Oct 17, 2026
Thank you for your investigation and for sharing your thoughts.
Based on your findings and the SDK documentation, I think this change could solve not only a technical inconsistency but also real problems for users.
At the same time, changing the mapping could break existing codebases. Some users may have added ad hoc workarounds for the current behavior. After the change, their code might raise an exception or return a completely different value, causing their business logic to fail.
For this reason, I think we need to handle this migration carefully.
Record sizes and field offsets are important for technologies based on
ctypes, because they help us avoid breaking low-level memory layouts.Since both
char_typeandschar_typeproduce the same record sizes and field offsets in this case, I do not think we need to be too concerned about this particular risk. We should still investigate Python calling compatibility separately.We have to rely on the type information available to us. We cannot know from the type library alone whether an application actually uses every affected member, or what values a
VARIANTwill return in practice, because that depends on the implementation.Nevertheless, I think parsing the type library is the most practical starting point for detecting potential compatibility issues. A warning at this point would tell users that they may need to investigate their applications before changing the mapping.
I have a small hope that the noise could encourage users to pay attention to the issue and investigate its impact on their applications.
The purpose of this warning is not simply to notify users about a technical change. It is to help them find out whether the change affects their own code.
Some noise may be acceptable if it helps users discover problems before upgrading.
From another perspective, warnings are not emitted on every import if the generated module is reused. This can help prevent unnecessary noise.
comtypeshas a version checker for generated modules.When the version of
comtypeschanges and a cached module under thegendirectory was generated by an incompatible version, anImportErroris raised. The client catches this error, parses the library, emits warnings and regenerates the module.I understand this concern. However, users who run Python with
-W errorhave explicitly chosen to treat warnings as errors.They can use
warnings.filterwarnings()or an equivalent mechanism to filter this specific warning if they decide that it should not stop their application.If the built-in
UserWarningcategory is too broad, we may need a custom warning class that inherits from a built-in warning category but adds no methods or attributes.In practice, I am considering a feature flag that lets users choose which mapping they want to use. This would make the behavior consistent across the type libraries being parsed.
If users need different mappings for individual fields or arguments, they may have to maintain their own record or interface definitions in their project.
We should discuss whether such libraries and use cases actually exist, and whether supporting them would be reasonable.
I agree that a warning alone is not a complete compatibility solution. In addition to warnings, I am considering introducing a feature flag to make the migration more manageable.
Here is what I found about the import process.
Importing
comtypesdoes not indirectly importcomtypes.tools.In a typical use case,
comtypes.toolsis initialized when the user importscomtypes.client.For example, the following code shows the additional modules loaded by
import comtypes.client:Based on this import flow, I am considering defining a flag such as
_PARSE_VT_I1_AS_SCHAR_in a place accessible after the initialimport comtypes.When
comtypes.clientis imported, the parser could read the flag and select the mapping that will remain in effect for the lifetime of the Python interpreter.For example:
The mapping in
tlbparser.pywould then look something like this:This is only a proposal, and I would like to hear your thoughts on it.
You are right. I overlooked this case.
Overall, I think the mapping change is technically justified, but we should make the compatibility risks visible and give users a practical way to control the transition.
I would like to discuss the warning and feature-flag approaches further before deciding on the migration strategy.
Thank you!
haoxuanjng-lang commentedon Oct 18, 2026
I agree with retaining the current default and making an opt-in transition a separate step. #962 now keeps
COMTYPES[VT_I1]unchanged and records the current failures and handwritten proposed behavior; its migration page has been removed.An interpreter-wide flag selected before importing
comtypes.clientsounds easier to explain and test than per-field choices. I do not have evidence of an application that needs different mappings for individual fields, so I would not add that mechanism based on the library survey alone.The generated-module cache needs an explicit policy alongside the flag. In the current source,
GetModulereturns_get_existing_module(tlib)before invoking the generator, and generated names depend on library identity/version, without a mapping selector. Selecting a new parser mapping therefore does not itself change an existing generated module. Even a fresh interpreter can load wrappers written under the other setting. We should either isolate/identify caches by the selected mapping or document and test a regeneration procedure, including imported dependent libraries; once wrappers are loaded, changing the setting should not imply they have changed. A later test could generate the same library under each setting in separate processes with empty and reused caches, then assert actual declarations and calls.Your point about
-W erroris fair: that is the user's explicit policy, and a dedicated warning category gives them a narrow filter. My concern is documenting the behavior and allowing a targeted filter, not overriding that policy. A summary versus detailed member reporting can remain a separate diagnostics choice; the mapping flag controls semantics.The previous real-library survey remains declaration/local-record evidence, not an end-to-end SAPI/PortableDevice/CLR/event test. The bytes-versus-integer field values and counted-buffer truncation show why preserving defaults during this discussion is useful, but do not establish prevalence or prove a complete user operation is fixed.
This is feedback on the proposal, not an implementation or validation of a flag. AI assistance: OpenAI Codex.
junkmd commentedon Oct 19, 2026
Thank you so much for your deep research and insights.
I would like to share some thoughts and ideas from a maintainer's point of view.
If we try to completely separate caches for each mapping, it will make the code too complex to maintain. Compared to choices like the No-GIL versus normal versions of CPython (where the massive benefits justify the complexity), this kind of opt-in feature for the cache doesn't seem to offer enough benefit to justify the extra complexity.
Instead, as a way to identify them, we think it would be good enough if the code generator just adds a small comment in the generated files, like this:
# This code was generated with comtypes=={comtypes.__version__}, _PARSE_VT_I1_AS_SCHAR_=={comtypes._PARSE_VT_I1_AS_SCHAR_}For the regeneration guide, we can explain that users should do the following steps in the first
__init__.pyfile when their app starts up:comtypes.clear_cache)import comtypesand set the flag (for example,comtypes._PARSE_VT_I1_AS_SCHAR_ = ...)import comtypes.clientand callclient.GetModule(...)as neededHowever,
comtypes.clear_cacheis designed for command-line use rather than being called from scripts, so we would need to refactor it if applications want to call it from their code. Also, because it importsclientinternally, they need to set the mapping flag after importingcomtypesand before callingclear_cache.If a project uses a package like
pywinauto(which needs changes tosys.coinit_flagsto work), the application side will need to handle the correct startup order.Example:
Even if we only support an interpreter-wide flag instead of flags for each library or parameter, this test is still possible.
For the tests, we think the test helper
patch_gen_dir(which we already made intest_client_regenerate_modules.py) will be very helpful.In a similar way, we can temporarily remove
toolsandgenfrom the import system, patch the flag, and callGetModuleto test the flag changes safely in separate processes.However, since import system helpers can get pretty complicated, we should add good comments explaining how and why it works, along with clear
assertchecks to make sure it behaves as expected.Do you have any ideas for what built-in type to use as the base for the custom warning, or what name we should give it?
If there are any technical blockers that stop us from running E2E tests for SAPI, PortableDevice, CLR, or events on CI, could you please let us know?