Skip to content

python: improve typing of static properties (classproperty) #5259

Closed

Description

@mrgrain

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.

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 --strict and pyright, with no ignores: Foo.static_array and Foo().static_array are both list[str], and Foo().static_array = 42 is rejected.
  • At runtime sget/sset receive Foo.

Open points for this shape:

  • classproperty needs 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 where jsii.set(self, ...) is correctly typed. It's only needed for static setters, and with (1) not even there.
  • reportGeneralTypeIssues on 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

  1. added
    bugThis issue is a bug.
    needs-triageThis issue or PR still needs to be triaged.
    and removed
    needs-triageThis issue or PR still needs to be triaged.
    on Oct 6, 2026
  2. mrgrain commented on Oct 13, 2026

    @mrgrain
    ContributorAuthor
  3. github-actions commented on Oct 13, 2026

    @github-actions
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis issue is a bug.p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions