gh-142750: Normalize ParamSpec __bound__ when bound is None
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
- Issue: gh-142750
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
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?
Yes, we decided to not care, because bound=None just means None :)