Repository navigation
Add type stubs for Cython classes in pywt._extensions #846
Copy link
Copy link
Open
Labels
Description
Currently, many public classes and functions are implemented in the Cython modules inside pywt._extensions (e.g. wavelist, Wavelet, ...). Their public API is currently documented in the pywavelet docs; however, when writing Python code that uses these functions or classes Python type checkers such as mypy or Pyright can not determine their signature and return types or even detect their existence.
Possible solution
To enable static type checking I propose to add .pyi interface files in the pywt._extensions module. I have already created a draft version that works in my locally built pywavelets installation. Please let me know if you think this would be a useful enhancement for the package.
# pywt/_extensions/_pywt.pyi
from typing import Literal, Optional
_WaveletFamily = Literal[
"haar",
"db",
"sym",
"coif",
"bior",
"rbio",
"dmey",
"gaus",
"mexh",
"morl",
"cgau",
"shan",
"fbsp",
"cmor",
]
_Kind = Literal["all", "continuous", "discrete"]
_Symmetry = Literal[
"asymmetric",
"near symmetric",
"symmetric",
"anti-symmetric",
"unknown",
]
def wavelist(family: _WaveletFamily | None = ..., kind: _Kind = ...) -> list[str]: ...
def families(short: bool = ...) -> list[str]: ...
class Wavelet:
def __init__(self, name: str, filter_bank=None): ...
def __len__(self) -> int: ...
@property
def name(self) -> str: ...
@property
def dec_lo(self) -> list[float]: ...
@property
def dec_hi(self) -> list[float]: ...
@property
def rec_lo(self) -> list[float]: ...
@property
def rec_hi(self) -> list[float]: ...
@property
def rec_len(self) -> int: ...
@property
def dec_len(self) -> int: ...
@property
def family_number(self) -> int: ...
@property
def family_name(self) -> str: ...
@property
def short_family_name(self) -> str: ...
@property
def orthogonal(self) -> bool: ...
@orthogonal.setter
def orthogonal(self, value: bool) -> None: ...
@property
def biorthogonal(self) -> bool: ...
@biorthogonal.setter
def biorthogonal(self, value: bool) -> None: ...
@property
def symmetry(self) -> _Symmetry: ...
@property
def vanishing_moments_psi(self) -> int | None: ...
@property
def vanishing_moments_phi(self) -> int | None: ...
@property
def filter_bank(
self,
) -> tuple[list[float], list[float], list[float], list[float]]: ...
def get_filters_coeffs(
self,
) -> tuple[list[float], list[float], list[float], list[float]]: ...
@property
def inverse_filter_bank(
self,
) -> tuple[list[float], list[float], list[float], list[float]]: ...
def get_reverse_filters_coeffs(
self,
) -> tuple[list[float], list[float], list[float], list[float]]: ...
class ContinuousWavelet:
@property
def family_number(self) -> int: ...
@property
def family_name(self) -> str: ...
@property
def short_family_name(self) -> str: ...
@property
def orthogonal(self) -> bool: ...
@orthogonal.setter
def orthogonal(self, value: bool) -> None: ...
@property
def biorthogonal(self) -> bool: ...
@biorthogonal.setter
def biorthogonal(self, value: bool) -> None: ...
@property
def complex_cwt(self) -> bool: ...
@complex_cwt.setter
def complex_cwt(self, value: bool) -> None: ...
@property
def lower_bound(self) -> Optional[float]: ...
@lower_bound.setter
def lower_bound(self, value: float) -> None: ...
@property
def upper_bound(self) -> Optional[float]: ...
@upper_bound.setter
def upper_bound(self, value: float) -> None: ...
@property
def center_frequency(self) -> Optional[float]: ...
@center_frequency.setter
def center_frequency(self, value: float) -> None: ...
@property
def bandwidth_frequency(self) -> Optional[float]: ...
@bandwidth_frequency.setter
def bandwidth_frequency(self, value: float) -> None: ...
@property
def fbsp_order(self) -> Optional[int]: ...
@fbsp_order.setter
def fbsp_order(self, value: int) -> None: ...
@property
def symmetry(self) -> _Symmetry: ...Reactions are currently unavailable
Activity
rgommers commentedon Apr 9, 2026
Hi @NiklasMueller2001, thanks for suggesting this, that seems like a good thing to add indeed. This may be a bit of a project given the size of the API and the support for fused types (32/64-bit float and complex dtypes) in the core functions. It might fit in one large PR, or be broken into multiple PRs - whatever seems best to you.
Ideally it'd come with a CI job to verify that the added stubs are correct and complete. Given
pyrightis now unmaintained andmypyis slow and under-maintained, I think I'd prefer usingpyrefly(it has a coverage report now: https://pyrefly.org/en/docs/report/).NiklasMueller2001 commentedon Apr 10, 2026
I would preferably create one large PR and add commits incrementally to it. Just to clarify the scope of this project, when looking through the pywt._extensions module I identified the following functions/classes that could use some stub files:
_cwt.pyx:_dwt.pyx:_pywt.pyx:_swt.pyx:rgommers commentedon Apr 10, 2026
That plan sounds good to me. Those are the most important functions indeed. There is more in pure Python code though that is also untyped (everything in https://pywavelets.readthedocs.io/en/latest/ref/index.html). Perhaps that can be inferred easily though, e.g. with https://pyrefly.org/en/docs/stubgen/. It can also be left for later if the scope of that is too much.
rgommers commentedon Apr 13, 2026
@jorenham pointed out to me that
__all__being dynamic prevents typecheckers from figuring out what is public, so changing that to a regular static list could already be quite helpful to see the API surface:pywt/pywt/__init__.py
Line 26 in 45f51e3
NiklasMueller2001 commentedon Apr 14, 2026
Interestingly, the API of public functions seem to be correctly propagated even with the current dynamic version with wildcard imports like this:
pywt/pywt/__init__.py
Lines 14 to 15 in 45f51e3
When import from
pywtI can see the signature of public functions correctly:NiklasMueller2001 commentedon Apr 15, 2026
I opened a draft PR now (#847) that already implements the stub files and removes wildcard imports in
pywt/__init__.py. As for the CI job, pyrefly reports over 200 errors that originate from core python modules independent of the untyped Cython modules. So how should we handle this? One idea would be to fix the current state of the pyrefly report using pyrefly baselines and let the job fail whenever a new type error is introduced.rgommers commentedon Apr 16, 2026
I'm fine with failing on new type errors in CI. For the Pyrefly baselines idea: looks reasonable at first sight, if we can't fix those errors up. NumPy and SciPy are fully typed though, and I wouldn't expect to see typing errors from stdlib usage. So let's see first in how far we can get with fixing up those errors?
NiklasMueller2001 commentedon Apr 17, 2026
Yeah, I will give Pyrefly's stubgen a shot. Can I add those type hinting additions to pure python modules also to the same PR?
NiklasMueller2001 commentedon May 20, 2026
Hi, I added a lot of type annotation to the pure python files to achieve better coverage and created a CLI workflow to check coverage regressions. Unfortunately, pyrefly still reports hundreds of type errors inside quite convoluted functions, due to imprecise interfaces / complicated branching logic. In total the errors amount to 310. I would find it very tedious to try to manually clean this up without breaking some edge cases. That's why I stuck to type hints and coverage reports in the PR.