Skip to content

Conversation

@hyongtao-code
Copy link
Contributor

@hyongtao-code hyongtao-code commented Dec 15, 2025

When creating a ParamSpec with bound=None, the runtime bound attribute is set to <class 'NoneType'> instead of None.

This change mirrors TypeVar.new by explicitly converting Py_None to NULL before type checking, ensuring consistency between TypeVar and ParamSpec.

Steps to reproduce

from typing import ParamSpec, Callable

P1 = ParamSpec("P")
P2 = ParamSpec("P", bound=None)
P3 = ParamSpec("P", bound=Callable[[int, str], float])

def check_bound(label, p, expected):
    got = p.__bound__
    ok = (got is expected)
    exp_s = repr(expected)
    got_s = repr(got)
    print(f"{label}.__bound__ should be {exp_s}, got {got_s}  ->  {'OK' if ok else 'FAIL'}")

check_bound("P1", P1, None)
check_bound("P2", P2, None)
check_bound("P3", P3, Callable[[int, str], float])

Result without the patch

d:\MyCode\cpython\PCbuild\amd64>python_d.exe py_bound.py
P1.__bound__ should be None, got <class 'NoneType'>  ->  FAIL
P2.__bound__ should be None, got <class 'NoneType'>  ->  FAIL
P3.__bound__ should be typing.Callable[[int, str], float], got typing.Callable[[int, str], float]  ->  OK

Result with the patch

d:\MyCode\cpython\PCbuild\amd64>python_d.exe py_bound.py
P1.__bound__ should be None, got None  ->  OK
P2.__bound__ should be None, got None  ->  OK
P3.__bound__ should be typing.Callable[[int, str], float], got typing.Callable[[int, str], float]  ->  OK

When creating a ParamSpec with bound=None, the runtime __bound__
attribute is set to <class 'NoneType'> instead of None.

This change mirrors TypeVar.__new__ by explicitly converting Py_None
to NULL before type checking, restoring the correct semantics and
ensuring consistency between TypeVar and ParamSpec.

Signed-off-by: Yongtao Huang <[email protected]>
Copy link
Member

@JelleZijlstra JelleZijlstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I agree we should change this. Can you add a test?

Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. None is the default value listed in the docs: https://docs.python.org/3/library/typing.html#typing.ParamSpec So, it should not be set to NoneType 100%

Please, add a test case and add a news entry :)

@hyongtao-code
Copy link
Contributor Author

Thanks for the review! 🙂
I've added a NEWS entry and a test case to cover the ParamSpec(..., bound=None) behavior.

Test case passed locally.

D:\MyCode\cpython>PCbuild\amd64\python_d.exe -m test test_typing.py
Using random seed: 3794058828
0:00:00 Run 1 test sequentially in a single process
0:00:00 [1/1] test_typing
0:00:02 [1/1] test_typing passed

== Tests result: SUCCESS ==

1 test OK.

Total duration: 2.0 sec
Total tests: run=715 skipped=1
Total test files: run=1/1
Result: SUCCESS

@JelleZijlstra
Copy link
Member

Thinking about this more, did this case ever come up with TypeVar?

This seems sort of wrong:

>>> repr(TypeVar("T").__bound__)
'None'
>>> repr(TypeVar("T", bound=None).__bound__)
'None'

Having no bound and having a bound of None mean very different things to a type checker, yet they have the same runtime representation. I feel like this came up before and maybe we just decided not to care, since a bound of None is not very useful at type checking time?

@sobolevn
Copy link
Member

Yes, we decided to not care, because bound=None just means None :)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants