Repository: python/typeshed
Pull request #13777: `_type_` class attributes in `ctypes`, and fix `ctypes.wintypes.BYTE`
Author: login: jorenham
Created: 2025-04-02T20:54:30Z | Merged: 2025-04-07T12:16:11Z

This adds the `_type_` class attributes to the relevant types of `ctypes`. It makes the individual `c_*` types  structurally distinguishable. For the integer and floating C types, the C99 standard is assumed:  https://en.cppreference.com/w/c/language/arithmetic_types#Integer_types as reference

## But why?

A `Protocol` can now be used to e.g. only accept C types *could* have  `_type_ == "l"`, i.e. signed integers of at least 32 bits on 32- and 64-bits systems. This will match `c_int`, `c_long`, `c_longlong`, `c_int32`, `c_int64`, `c_time_t`, `c_ssize_t`, and on windows also `HRESULT`. The alternative would be to use a platform-specific union of these 7 or 8 types, which would be rather messy and verbose.

Also, because stubs are often used as form of documentation (or maybe that's just me), this will also help in that sense.




Diff:
diff --git a/stdlib/ctypes/__init__.pyi b/stdlib/ctypes/__init__.pyi
index a7e9c1ce068c..a7e19483301c 100644
--- a/stdlib/ctypes/__init__.pyi
+++ b/stdlib/ctypes/__init__.pyi
@@ -27,8 +27,8 @@ from _ctypes import (
 from _typeshed import StrPath
 from ctypes._endian import BigEndianStructure as BigEndianStructure, LittleEndianStructure as LittleEndianStructure
 from types import GenericAlias
-from typing import Any, ClassVar, Generic, type_check_only
-from typing_extensions import Self, TypeAlias, TypeVar, deprecated
+from typing import Any, ClassVar, Generic, Literal, TypeVar, type_check_only
+from typing_extensions import Self, TypeAlias, deprecated
 
 if sys.platform == "win32":
     from _ctypes import FormatError as FormatError, get_last_error as get_last_error, set_last_error as set_last_error
@@ -186,73 +186,121 @@ if sys.platform == "win32":
 
 def wstring_at(ptr: _CVoidConstPLike, size: int = -1) -> str: ...
 
-class c_byte(_SimpleCData[int]): ...
+class py_object(_CanCastTo, _SimpleCData[_T]):
+    _type_: ClassVar[Literal["O"]]
+
+class c_bool(_SimpleCData[bool]):
+    _type_: ClassVar[Literal["?"]]
+    def __init__(self, value: bool = ...) -> None: ...
+
+class c_byte(_SimpleCData[int]):
+    _type_: ClassVar[Literal["b"]]
+
+class c_ubyte(_SimpleCData[int]):
+    _type_: ClassVar[Literal["B"]]
+
+class c_short(_SimpleCData[int]):
+    _type_: ClassVar[Literal["h"]]
+
+class c_ushort(_SimpleCData[int]):
+    _type_: ClassVar[Literal["H"]]
+
+class c_long(_SimpleCData[int]):
+    _type_: ClassVar[Literal["l"]]
+
+class c_ulong(_SimpleCData[int]):
+    _type_: ClassVar[Literal["L"]]
+
+class c_int(_SimpleCData[int]):  # can be an alias for c_long
+    _type_: ClassVar[Literal["i", "l"]]
+
+class c_uint(_SimpleCData[int]):  # can be an alias for c_ulong
+    _type_: ClassVar[Literal["I", "L"]]
+
+class c_longlong(_SimpleCData[int]):  # can be an alias for c_long
+    _type_: ClassVar[Literal["q", "l"]]
+
+class c_ulonglong(_SimpleCData[int]):  # can be an alias for c_ulong
+    _type_: ClassVar[Literal["Q", "L"]]
+
+c_int8 = c_byte
+c_uint8 = c_ubyte
+
+class c_int16(_SimpleCData[int]):  # can be an alias for c_short or c_int
+    _type_: ClassVar[Literal["h", "i"]]
+
+class c_uint16(_SimpleCData[int]):  # can be an alias for c_ushort or c_uint
+    _type_: ClassVar[Literal["H", "I"]]
+
+class c_int32(_SimpleCData[int]):  # can be an alias for c_int or c_long
+    _type_: ClassVar[Literal["i", "l"]]
+
+class c_uint32(_SimpleCData[int]):  # can be an alias for c_uint or c_ulong
+    _type_: ClassVar[Literal["I", "L"]]
+
+class c_int64(_SimpleCData[int]):  # can be an alias for c_long or c_longlong
+    _type_: ClassVar[Literal["l", "q"]]
+
+class c_uint64(_SimpleCData[int]):  # can be an alias for c_ulong or c_ulonglong
+    _type_: ClassVar[Literal["L", "Q"]]
+
+class c_ssize_t(_SimpleCData[int]):  # alias for c_int, c_long, or c_longlong
+    _type_: ClassVar[Literal["i", "l", "q"]]
+
+class c_size_t(_SimpleCData[int]):  # alias for c_uint, c_ulong, or c_ulonglong
+    _type_: ClassVar[Literal["I", "L", "Q"]]
+
+class c_float(_SimpleCData[float]):
+    _type_: ClassVar[Literal["f"]]
+
+class c_double(_SimpleCData[float]):
+    _type_: ClassVar[Literal["d"]]
+
+class c_longdouble(_SimpleCData[float]):  # can be an alias for c_double
+    _type_: ClassVar[Literal["d", "g"]]
+
+if sys.version_info >= (3, 14):
+    class c_float_complex(_SimpleCData[complex]):
+        _type_: ClassVar[Literal["E"]]
+
+    class c_double_complex(_SimpleCData[complex]):
+        _type_: ClassVar[Literal["C"]]
+
+    class c_longdouble_complex(_SimpleCData[complex]):
+        _type_: ClassVar[Literal["F"]]
 
 class c_char(_SimpleCData[bytes]):
+    _type_: ClassVar[Literal["c"]]
     def __init__(self, value: int | bytes | bytearray = ...) -> None: ...
 
 class c_char_p(_PointerLike, _SimpleCData[bytes | None]):
+    _type_: ClassVar[Literal["z"]]
     def __init__(self, value: int | bytes | None = ...) -> None: ...
     @classmethod
     def from_param(cls, value: Any, /) -> Self | _CArgObject: ...
 
-class c_double(_SimpleCData[float]): ...
-class c_longdouble(_SimpleCData[float]): ...  # can be an alias for c_double
-class c_float(_SimpleCData[float]): ...
-class c_int(_SimpleCData[int]): ...  # can be an alias for c_long
-class c_long(_SimpleCData[int]): ...
-class c_longlong(_SimpleCData[int]): ...  # can be an alias for c_long
-class c_short(_SimpleCData[int]): ...
-class c_size_t(_SimpleCData[int]): ...  # alias for c_uint, c_ulong, or c_ulonglong
-class c_ssize_t(_SimpleCData[int]): ...  # alias for c_int, c_long, or c_longlong
-class c_ubyte(_SimpleCData[int]): ...
-class c_uint(_SimpleCData[int]): ...  # can be an alias for c_ulong
-class c_ulong(_SimpleCData[int]): ...
-class c_ulonglong(_SimpleCData[int]): ...  # can be an alias for c_ulong
-class c_ushort(_SimpleCData[int]): ...
-
 class c_void_p(_PointerLike, _SimpleCData[int | None]):
+    _type_: ClassVar[Literal["P"]]
     @classmethod
     def from_param(cls, value: Any, /) -> Self | _CArgObject: ...
 
 c_voidp = c_void_p  # backwards compatibility (to a bug)
 
-class c_wchar(_SimpleCData[str]): ...
-
-c_int8 = c_byte
-
-# these are actually dynamic aliases for c_short, c_int, c_long, or c_longlong
-class c_int16(_SimpleCData[int]): ...
-class c_int32(_SimpleCData[int]): ...
-class c_int64(_SimpleCData[int]): ...
-
-c_uint8 = c_ubyte
-
-# these are actually dynamic aliases for c_ushort, c_uint, c_ulong, or c_ulonglong
-class c_uint16(_SimpleCData[int]): ...
-class c_uint32(_SimpleCData[int]): ...
-class c_uint64(_SimpleCData[int]): ...
+class c_wchar(_SimpleCData[str]):
+    _type_: ClassVar[Literal["u"]]
 
 class c_wchar_p(_PointerLike, _SimpleCData[str | None]):
+    _type_: ClassVar[Literal["Z"]]
     def __init__(self, value: int | str | None = ...) -> None: ...
     @classmethod
     def from_param(cls, value: Any, /) -> Self | _CArgObject: ...
 
-class c_bool(_SimpleCData[bool]):
-    def __init__(self, value: bool = ...) -> None: ...
-
 if sys.platform == "win32":
-    class HRESULT(_SimpleCData[int]): ...  # TODO: undocumented
+    class HRESULT(_SimpleCData[int]):  # TODO: undocumented
+        _type_: ClassVar[Literal["l"]]
 
 if sys.version_info >= (3, 12):
     # At runtime, this is an alias for either c_int32 or c_int64,
-    # which are themselves an alias for one of c_short, c_int, c_long, or c_longlong
+    # which are themselves an alias for one of c_int, c_long, or c_longlong
     # This covers all our bases.
-    c_time_t: type[c_int32 | c_int64 | c_short | c_int | c_long | c_longlong]
-
-class py_object(_CanCastTo, _SimpleCData[_T]): ...
-
-if sys.version_info >= (3, 14):
-    class c_float_complex(_SimpleCData[complex]): ...
-    class c_double_complex(_SimpleCData[complex]): ...
-    class c_longdouble_complex(_SimpleCData[complex]): ...
+    c_time_t: type[c_int32 | c_int64 | c_int | c_long | c_longlong]
diff --git a/stdlib/ctypes/wintypes.pyi b/stdlib/ctypes/wintypes.pyi
index e938d8f22957..63f117787aa0 100644
--- a/stdlib/ctypes/wintypes.pyi
+++ b/stdlib/ctypes/wintypes.pyi
@@ -1,10 +1,10 @@
+import sys
 from _ctypes import _CArgObject, _CField
 from ctypes import (
     Array,
     Structure,
     _Pointer,
     _SimpleCData,
-    c_byte,
     c_char,
     c_char_p,
     c_double,
@@ -24,7 +24,15 @@ from ctypes import (
 from typing import Any, TypeVar
 from typing_extensions import Self, TypeAlias
 
-BYTE = c_byte
+if sys.version_info >= (3, 12):
+    from ctypes import c_ubyte
+
+    BYTE = c_ubyte
+else:
+    from ctypes import c_byte
+
+    BYTE = c_byte
+
 WORD = c_ushort
 DWORD = c_ulong
 CHAR = c_char


Reviews:
user: login: brianschubert
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T21:11:26Z
user: login: jorenham
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T21:14:23Z
user: login: srittau
body: There's a merge conflict now, but apart from that LGTM – for my limited ctypes experience.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T08:12:59Z

Review comments:
diff_hunk: @@ -24,7 +24,7 @@ from ctypes import (
 from typing import Any, TypeVar
 from typing_extensions import Self, TypeAlias
 
-BYTE = c_byte
+BYTE = c_ubyte
path: stdlib/ctypes/wintypes.pyi
user: login: brianschubert
body: Looks like this changed in 3.12: https://github.com/python/cpython/pull/97579
```suggestion
if sys.version_info >= (3, 12):
    BYTE = c_ubyte
else:
    BYTE = c_byte
```
created_at: 2025-04-02T21:11:26Z
author_association: COLLABORATOR
diff_hunk: @@ -24,7 +24,7 @@ from ctypes import (
 from typing import Any, TypeVar
 from typing_extensions import Self, TypeAlias
 
-BYTE = c_byte
+BYTE = c_ubyte
path: stdlib/ctypes/wintypes.pyi
user: login: jorenham
body: Yea I just noticed as well, after comparing stubtest outcomes. Should be fixed now
created_at: 2025-04-02T21:14:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: jorenham
created_at: 2025-04-02T21:05:44Z
body: Another advantage: stubtest is now able to recognize that `BYTE = c_byte` in `ctypes.wintypes` is incorrect :)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-02T21:05:50Z
body: According to [mypy_primer](https://github.com/hauntsaninja/mypy_primer), this change has no effect on the checked open source code. 🤖🎉
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-02T21:25:23Z
body: Diff from [mypy_primer](https://github.com/hauntsaninja/mypy_primer), showing the effect of this PR on open source code:
```diff
comtypes (https://github.com/enthought/comtypes)
+ comtypes/GUID.py:4: error: Module "ctypes.wintypes" does not explicitly export attribute "BYTE"  [attr-defined]

```
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-02T21:39:14Z
body: According to [mypy_primer](https://github.com/hauntsaninja/mypy_primer), this change has no effect on the checked open source code. 🤖🎉
author_association: CONTRIBUTOR
user: login: jorenham
created_at: 2025-04-03T13:39:27Z
body: > but apart from that LGTM – for my limited ctypes experience.

@seberg you're pretty C-savvy; does this look right to you?
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T13:47:39Z
body: According to [mypy_primer](https://github.com/hauntsaninja/mypy_primer), this change has no effect on the checked open source code. 🤖🎉
author_association: CONTRIBUTOR
user: login: brianschubert
created_at: 2025-04-03T15:08:13Z
body: Would it make sense for these to be `Final`?
author_association: COLLABORATOR
user: login: jorenham
created_at: 2025-04-03T15:36:18Z
body: > Would it make sense for these to be `Final`?

Good question. I guess that partially depends on whether mixed `ClassVar` and `Final` generally supported by the major type-checkers yet.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-07T12:15:49Z
body: According to [mypy_primer](https://github.com/hauntsaninja/mypy_primer), this change has no effect on the checked open source code. 🤖🎉
author_association: CONTRIBUTOR