Repository navigation
python: improve typing of static properties (classproperty) #5259
Description
Describe the bug
Generated Python code for static properties isn't typed correctly. The generator works around it with type-checker ignores, and readers of static properties get Any. mypy 2.4 now type-checks the static setter bodies, which exposes one of these problems (#5253). This issue lists everything found while investigating it.
Static properties are currently generated like this:
@jsii.python.classproperty
@jsii.member(jsii_name="staticArray")
def static_array(cls) -> typing.List[builtins.str]: # pyright: ignore [reportGeneralTypeIssues,reportRedeclaration]
return typing.cast(typing.List[builtins.str], jsii.sget(cls, "staticArray"))
@static_array.setter # type: ignore[no-redef]
def static_array(cls, value: typing.List[builtins.str]) -> None:
jsii.sset(cls, "staticArray", value) # pyright: ignore[reportArgumentType]1. cls is inferred as an instance, not a class
The accessors are plain functions. Both mypy and pyright decide the type of the first parameter from how a function is decorated, and only @classmethod makes it type[Self]. A custom decorator like classproperty can't change that, however it's typed. So cls is inferred as Foo, and jsii.sset(cls, ...) / jsii.sget(cls, ...) (which take klass: Type) are type errors.
- pyright: suppressed with
# pyright: ignore[reportArgumentType](and likelyreportGeneralTypeIssueson the getter). - mypy ≤ 2.3: never type-checked these setter bodies in single-process mode, so it reported nothing.
- mypy 2.4: now checks them (Use two-phase type checking in sequential mode python/mypy#21973) and reports
Argument 1 has incompatible type "Foo"; expected "type[Any]" [arg-type]. This breakstarget-python.test.jsin feat(deps-dev): bump mypy from 2.3.1 to 2.4.0 in /packages/jsii-pacmak/test/generated-code #5253.
Fix: stack @builtins.classmethod under the property decorators. The runtime already accepts classmethod objects (classproperty() and .setter() only wrap the function if it isn't one already). Generated static methods already apply jsii.member on top of builtins.classmethod the same way.
@jsii.python.classproperty
@jsii.member(jsii_name="staticArray")
@builtins.classmethod
def static_array(cls) -> typing.List[builtins.str]: # pyright: ignore [reportRedeclaration]
return typing.cast(typing.List[builtins.str], jsii.sget(cls, "staticArray"))
@static_array.setter # type: ignore[no-redef]
@builtins.classmethod
def static_array(cls, value: typing.List[builtins.str]) -> None:
jsii.sset(cls, "staticArray", value)Checked with mypy 2.4 --strict and pyright: no arg-type / reportArgumentType errors, and sget/sset receive the class at runtime. This is the minimal fix that unblocks the mypy upgrade.
2. Reading a static property gives Any
_ClassProperty.__get__ returns Any, so Foo.static_array and Foo.CONST are untyped for consumers.
Fix: make _ClassProperty generic, with the accessor parameters typed as callables (both checkers present a classmethod passed into another decorator as a plain callable that takes cls, not as a classmethod object, so annotating them as classmethod[...] doesn't work):
T = TypeVar("T")
class _ClassProperty(Generic[T]):
def __init__(self, fget: Callable[[Any], T], fset: Optional[Callable[[Any, T], None]] = None): ...
def __get__(self, obj: Any, klass: Optional[type] = None) -> T: ...
def __set__(self, obj: Any, value: T) -> None: ...
def setter(self, fset: Callable[[Any, T], None]) -> "_ClassProperty[T]": ...
def classproperty(fget: Callable[[Any], T]) -> _ClassProperty[T]: ...With the current decorator shape, this fixes read-only static properties (constants) in both checkers, and mutable ones in pyright. For mutable ones mypy still loses the type (see 3).
3. The @x.setter redefinition pattern only works for builtins.property
Both checkers treat @x.setter as redefining the same property only for the built-in property. For classproperty that's why we need # type: ignore[no-redef] (mypy) and # pyright: ignore[reportRedeclaration] (pyright). With a generic _ClassProperty, mypy also types a mutable static property as an overloaded function instead of T, because it sees two definitions of the same name.
Fix: have the generator emit the accessors under separate private names and build the descriptor explicitly:
@builtins.classmethod
def _get_static_array(cls) -> typing.List[builtins.str]:
return typing.cast(typing.List[builtins.str], jsii.sget(cls, "staticArray"))
@builtins.classmethod
def _set_static_array(cls, value: typing.List[builtins.str]) -> None:
jsii.sset(cls, "staticArray", value)
static_array = jsii.python.classproperty(_get_static_array, _set_static_array)Prototyped with the generic _ClassProperty from (2):
- mypy 2.4
--strictand pyright, with no ignores:Foo.static_arrayandFoo().static_arrayare bothlist[str], andFoo().static_array = 42is rejected. - At runtime
sget/ssetreceiveFoo.
Open points for this shape:
classpropertyneeds to accept an optional setter argument.- The private accessor names have to be emitted without clashing with other members.
jsii.member(jsii_name=...)metadata has to be attached somewhere sensible.
4. Assigning at class level isn't type-checked by mypy
mypy doesn't route Foo.static_array = [...] through a descriptor's __set__; it checks it against the descriptor type itself. So even with (2) and (3), mypy rejects a valid class-level assignment (expression has type "list[str]", variable has type "_ClassProperty[list[str]]"). pyright handles it. This is a mypy limitation. Fully supporting it would need the descriptor on a per-class metaclass, which is probably not worth it. Documenting it may be enough.
5. Unnecessary ignores in generated code
# pyright: ignore[reportArgumentType]is emitted on every property setter (lib/targets/python.ts, setter body emission), including instance setters wherejsii.set(self, ...)is correctly typed. It's only needed for static setters, and with (1) not even there.reportGeneralTypeIssueson static getters should be unnecessary once (1) is in place.
Regression Issue
- Select this option if this issue appears to be a regression.
Expected Behavior
Generated static property accessors type-check under mypy --strict and pyright without ignores, and reading or assigning a static property is typed with the property's actual type.
Current Behavior
The generated code depends on type-checker ignores. mypy 2.4 reports arg-type errors in static setters (breaks #5253), and reading a static property gives Any.
Reproduction Steps
On the branch for #5253 (mypy 2.4.0):
cd packages/jsii-pacmak
yarn jest test/generated-code/target-python.test.js
jsii_calc/__init__.py:1135: error: Argument 1 has incompatible type "ClassWithCollections"; expected "type[Any]" [arg-type]
jsii.sset(cls, "staticArray", value) # pyright: ignore[reportA...
Possible Solution
See the numbered items above. (1) on its own unblocks the mypy 2.4 upgrade. (2) and (3) are the full typing fix and also change the public types consumers see on static properties: Any becomes real types, which may surface new type errors in downstream code. Related: #5258 (static property assignment doesn't call the setter at runtime), which touches the same _ClassProperty.__set__ / _ClassPropertyMeta.__setattr__ code.
Additional Information/Context
The variants were prototyped against mypy 2.4.0 --strict and the repo's pyright, with sget/sset stubbed to check runtime behaviour.
SDK version used
jsii 1.141.0 (main)
Environment details (OS name and version, etc.)
macOS, Python 3.13
Activity
mrgrain commentedon Oct 13, 2026
#5261
github-actions commentedon Oct 13, 2026
This issue is now closed. Comments on closed issues are hard for our team to see.
If you need more assistance, please open a new issue that references this one.