@@ -8,7 +8,8 @@ fixtures and `parametrize` — against the real package.
88pytest fills a test or fixture parameter by * name* from a registry: the function's own module, then
99the ` conftest.py ` chain, then builtin fixtures. A parameter annotation that disagrees with the
1010resolved fixture's type is an error; the fixture's provided type is its return annotation, with a
11- yield fixture's ` Iterator[T] ` / ` Generator[T, ...] ` unwrapped to ` T ` .
11+ yield fixture's ` Iterator[T] ` / ` Generator[T, ...] ` unwrapped to ` T ` . An * unannotated* parameter
12+ takes that provided type instead of the implicit ` Unknown ` an ordinary parameter would get.
1213
1314## Fixture resolution and type checking
1415
@@ -199,3 +200,270 @@ def fixture(function: None = ..., *, scope: str = ..., name: str | None = None)
199200def test_it (missing ) -> None : # error: [unknown-fixture]
200201 ...
201202```
203+
204+ ## Unannotated parameters take the fixture's type
205+
206+ An unannotated parameter is bound by pytest to the fixture of the same name, so the body sees that
207+ fixture's provided type — including through a rename, a yield fixture's unwrapping, and the
208+ ` conftest.py ` chain. A name that resolves to no fixture stays gradual, as does one whose fixture has
209+ no derivable type.
210+
211+ ``` toml
212+ [environment ]
213+ python = " /.venv"
214+ ```
215+
216+ ` /.venv/<path-to-site-packages>/pytest/__init__.pyi ` :
217+
218+ ``` pyi
219+ from _pytest.fixtures import fixture as fixture
220+ ```
221+
222+ ` /.venv/<path-to-site-packages>/_pytest/__init__.pyi ` :
223+
224+ ``` pyi
225+ ```
226+
227+ ` /.venv/<path-to-site-packages>/_pytest/fixtures.pyi ` :
228+
229+ ``` pyi
230+ from typing import Any, Callable, overload
231+
232+ class FixtureFunctionDefinition : ...
233+
234+ class FixtureFunctionMarker :
235+ def __call__ (self , function : Callable[... , Any]) -> FixtureFunctionDefinition: ...
236+
237+ @overload
238+ def fixture (function : Callable[... , Any], * , scope : str = ... , name : str | None = None ) -> FixtureFunctionDefinition: ...
239+ @overload
240+ def fixture (function : None = ... , * , scope : str = ... , name : str | None = None ) -> FixtureFunctionMarker: ...
241+ ```
242+
243+ ` conftest.py ` :
244+
245+ ``` py
246+ import pytest
247+
248+ @pytest.fixture
249+ def from_conftest () -> bytes :
250+ return b " "
251+ ```
252+
253+ ` test_unannotated.py ` :
254+
255+ ``` py
256+ import pytest
257+ from typing import Iterator
258+
259+ @pytest.fixture
260+ def number () -> int :
261+ return 1
262+
263+ @pytest.fixture (name = " renamed" )
264+ def _make_text () -> str :
265+ return " x"
266+
267+ @pytest.fixture
268+ def yielded () -> Iterator[bool ]:
269+ yield True
270+
271+ @pytest.fixture
272+ def untyped ():
273+ return object ()
274+
275+ def test_it (number , renamed , yielded , from_conftest , untyped , missing ) -> None : # error: [unknown-fixture]
276+ reveal_type(number) # revealed: int
277+ reveal_type(renamed) # revealed: str
278+ reveal_type(yielded) # revealed: bool
279+ reveal_type(from_conftest) # revealed: bytes
280+ reveal_type(untyped) # revealed: Unknown
281+ reveal_type(missing) # revealed: Unknown
282+ ```
283+
284+ ## A fixture's own parameters take fixture types
285+
286+ Fixtures request fixtures the same way tests do, so an unannotated parameter of a ` @pytest.fixture `
287+ function is typed from the registry too — including when the fixture it requests is defined later in
288+ the module.
289+
290+ ``` toml
291+ [environment ]
292+ python = " /.venv"
293+ ```
294+
295+ ` /.venv/<path-to-site-packages>/pytest/__init__.pyi ` :
296+
297+ ``` pyi
298+ from _pytest.fixtures import fixture as fixture
299+ ```
300+
301+ ` /.venv/<path-to-site-packages>/_pytest/__init__.pyi ` :
302+
303+ ``` pyi
304+ ```
305+
306+ ` /.venv/<path-to-site-packages>/_pytest/fixtures.pyi ` :
307+
308+ ``` pyi
309+ from typing import Any, Callable, overload
310+
311+ class FixtureFunctionDefinition : ...
312+
313+ class FixtureFunctionMarker :
314+ def __call__ (self , function : Callable[... , Any]) -> FixtureFunctionDefinition: ...
315+
316+ @overload
317+ def fixture (function : Callable[... , Any], * , scope : str = ... , name : str | None = None ) -> FixtureFunctionDefinition: ...
318+ @overload
319+ def fixture (function : None = ... , * , scope : str = ... , name : str | None = None ) -> FixtureFunctionMarker: ...
320+ ```
321+
322+ ` test_chained.py ` :
323+
324+ ``` py
325+ import pytest
326+
327+ @pytest.fixture
328+ def outer (inner ) -> str :
329+ reveal_type(inner) # revealed: int
330+ return str (inner)
331+
332+ @pytest.fixture
333+ def inner () -> int :
334+ return 1
335+
336+ def test_it (outer ) -> None :
337+ reveal_type(outer) # revealed: str
338+ ```
339+
340+ ## Parametrized names are arguments, not fixtures
341+
342+ ` @pytest.mark.parametrize ` supplies a name from its value rows rather than from the fixture
343+ registry, so a parametrized parameter is not fixture-typed even when a fixture of that name exists.
344+
345+ ``` toml
346+ [environment ]
347+ python = " /.venv"
348+ ```
349+
350+ ` /.venv/<path-to-site-packages>/pytest/__init__.pyi ` :
351+
352+ ``` pyi
353+ from _pytest.fixtures import fixture as fixture
354+ from _pytest.mark.structures import MarkGenerator as MarkGenerator
355+
356+ mark: MarkGenerator
357+ ```
358+
359+ ` /.venv/<path-to-site-packages>/_pytest/__init__.pyi ` :
360+
361+ ``` pyi
362+ ```
363+
364+ ` /.venv/<path-to-site-packages>/_pytest/mark/__init__.pyi ` :
365+
366+ ``` pyi
367+ ```
368+
369+ ` /.venv/<path-to-site-packages>/_pytest/mark/structures.pyi ` :
370+
371+ ``` pyi
372+ from typing import Any
373+
374+ class MarkDecorator :
375+ def __call__ (self , * args : Any, ** kwargs : Any) -> Any: ...
376+
377+ class MarkGenerator :
378+ def __getattr__ (self , name : str ) -> MarkDecorator: ...
379+ ```
380+
381+ ` /.venv/<path-to-site-packages>/_pytest/fixtures.pyi ` :
382+
383+ ``` pyi
384+ from typing import Any, Callable, overload
385+
386+ class FixtureFunctionDefinition : ...
387+
388+ class FixtureFunctionMarker :
389+ def __call__ (self , function : Callable[... , Any]) -> FixtureFunctionDefinition: ...
390+
391+ @overload
392+ def fixture (function : Callable[... , Any], * , scope : str = ... , name : str | None = None ) -> FixtureFunctionDefinition: ...
393+ @overload
394+ def fixture (function : None = ... , * , scope : str = ... , name : str | None = None ) -> FixtureFunctionMarker: ...
395+ ```
396+
397+ ` test_parametrized.py ` :
398+
399+ ``` py
400+ import pytest
401+
402+ @pytest.fixture
403+ def value () -> int :
404+ return 1
405+
406+ @pytest.mark.parametrize (" value" , [" a" , " b" ])
407+ def test_it (value ) -> None :
408+ # supplied by the marker, so the same-named fixture does not apply
409+ reveal_type(value) # revealed: Unknown
410+
411+ @pytest.mark.parametrize (" other" , [" a" , " b" ])
412+ def test_mixed (other , value ) -> None :
413+ reveal_type(other) # revealed: Unknown
414+ reveal_type(value) # revealed: int
415+ ```
416+
417+ ## Ordinary functions are untouched
418+
419+ A function pytest does not manage keeps the ordinary rules for an unannotated parameter, even when a
420+ fixture of that name is in scope.
421+
422+ ``` toml
423+ [environment ]
424+ python = " /.venv"
425+ ```
426+
427+ ` /.venv/<path-to-site-packages>/pytest/__init__.pyi ` :
428+
429+ ``` pyi
430+ from _pytest.fixtures import fixture as fixture
431+ ```
432+
433+ ` /.venv/<path-to-site-packages>/_pytest/__init__.pyi ` :
434+
435+ ``` pyi
436+ ```
437+
438+ ` /.venv/<path-to-site-packages>/_pytest/fixtures.pyi ` :
439+
440+ ``` pyi
441+ from typing import Any, Callable, overload
442+
443+ class FixtureFunctionDefinition : ...
444+
445+ class FixtureFunctionMarker :
446+ def __call__ (self , function : Callable[... , Any]) -> FixtureFunctionDefinition: ...
447+
448+ @overload
449+ def fixture (function : Callable[... , Any], * , scope : str = ... , name : str | None = None ) -> FixtureFunctionDefinition: ...
450+ @overload
451+ def fixture (function : None = ... , * , scope : str = ... , name : str | None = None ) -> FixtureFunctionMarker: ...
452+ ```
453+
454+ ` test_ordinary.py ` :
455+
456+ ``` py
457+ import pytest
458+
459+ @pytest.fixture
460+ def number () -> int :
461+ return 1
462+
463+ # a helper, not a test: pytest never calls it, so `number` is an ordinary parameter
464+ def helper (number ) -> None :
465+ reveal_type(number) # revealed: Unknown
466+
467+ def test_it () -> None :
468+ helper(" anything" )
469+ ```
0 commit comments