From fad004118511f69aa46472ac96f90b72377cf0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CUner=E2=80=9D?= Date: Tue, 30 Dec 2025 16:38:19 +0000 Subject: [PATCH 1/2] Improve error message for missing dependency type annotation --- fastapi/dependencies/utils.py | 32 +++++++++++++++++++++++++++--- fastapi/routing.py | 5 ++++- tests/test_dependency_overrides.py | 19 ++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 45e1ff3ed1..7b6006dba7 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -46,7 +46,7 @@ from fastapi.concurrency import ( contextmanager_in_threadpool, ) from fastapi.dependencies.models import Dependant -from fastapi.exceptions import DependencyScopeError +from fastapi.exceptions import DependencyScopeError, FastAPIError from fastapi.logger import logger from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey @@ -109,7 +109,9 @@ def ensure_multipart_is_installed() -> None: raise RuntimeError(multipart_not_installed_error) from None -def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant: +def get_parameterless_sub_dependant( + *, depends: params.Depends, path: str +) -> Dependant: assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" ) @@ -121,6 +123,7 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De call=depends.dependency, scope=depends.scope, own_oauth_scopes=own_oauth_scopes, + enforce_annotation=False, ) @@ -248,6 +251,13 @@ def get_typed_return_annotation(call: Callable[..., Any]) -> Any: return get_typed_annotation(annotation, globalns) +def _dependency_defines_type(depends: params.Depends) -> bool: + dependency = depends.dependency + if inspect.isclass(dependency): + return True + return get_typed_return_annotation(dependency) is not None + + def get_dependant( *, path: str, @@ -257,6 +267,7 @@ def get_dependant( parent_oauth_scopes: Optional[list[str]] = None, use_cache: bool = True, scope: Union[Literal["function", "request"], None] = None, + enforce_annotation: bool = True, ) -> Dependant: dependant = Dependant( call=call, @@ -281,6 +292,15 @@ def get_dependant( ) if param_details.depends is not None: assert param_details.depends.dependency + if ( + enforce_annotation + and param.annotation is inspect.Signature.empty + and not _dependency_defines_type(param_details.depends) + ): + raise FastAPIError( + f'Dependency parameter "{param_name}" must have a type annotation. ' + f'For example: `{param_name}: SomeType = Depends(...)`' + ) if ( (dependant.is_gen_callable or dependant.is_async_gen_callable) and dependant.computed_scope == "request" @@ -303,6 +323,7 @@ def get_dependant( parent_oauth_scopes=current_scopes, use_cache=param_details.depends.use_cache, scope=param_details.depends.scope, + enforce_annotation=False, ) dependant.dependencies.append(sub_dependant) continue @@ -315,7 +336,11 @@ def get_dependant( f"Cannot specify multiple FastAPI annotations for {param_name!r}" ) continue - assert param_details.field is not None + if enforce_annotation and param_details.field is None: + raise FastAPIError( + f'Dependency parameter "{param_name}" must have a type annotation. ' + f'For example: `{param_name}: SomeType = Depends(...)`' + ) if isinstance(param_details.field.field_info, params.Body): dependant.body_params.append(param_details.field) else: @@ -609,6 +634,7 @@ async def solve_dependencies( name=sub_dependant.name, parent_oauth_scopes=sub_dependant.oauth_scopes, scope=sub_dependant.scope, + enforce_annotation=False, ) solved_result = await solve_dependencies( diff --git a/fastapi/routing.py b/fastapi/routing.py index 9ca2f46732..39bc4afd18 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -456,7 +456,10 @@ class APIWebSocketRoute(routing.WebSocketRoute): self.dependencies = list(dependencies or []) self.path_regex, self.path_format, self.param_convertors = compile_path(path) self.dependant = get_dependant( - path=self.path_format, call=self.endpoint, scope="function" + path=self.path_format, + call=self.endpoint, + scope="function", + enforce_annotation=False, ) for depends in self.dependencies[::-1]: self.dependant.dependencies.insert( diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index e25db624d8..310cf51a50 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -2,6 +2,7 @@ from typing import Optional import pytest from fastapi import APIRouter, Depends, FastAPI +from fastapi.exceptions import FastAPIError from fastapi.testclient import TestClient app = FastAPI() @@ -389,3 +390,21 @@ def test_override_with_sub_router_decorator_depends_k_bar(): assert response.status_code == 200 assert response.json() == {"in": "router-decorator-depends"} app.dependency_overrides = {} + + +def test_missing_type_annotation_dependency(): + local_app = FastAPI() + + def get_db(): + return "db" + + with pytest.raises(FastAPIError) as exc: + @local_app.get("/bad") + def bad(dep=Depends(get_db)): + return dep + TestClient(local_app) + + msg = str(exc.value) + assert "Dependency parameter" in msg + assert "type annotation" in msg + assert "Depends" in msg From 4e2cfc24be0fe3a961a8c27f9bd378610b091dc3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:18:34 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 8 +++----- tests/test_dependency_overrides.py | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 7b6006dba7..ed717f63a4 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -109,9 +109,7 @@ def ensure_multipart_is_installed() -> None: raise RuntimeError(multipart_not_installed_error) from None -def get_parameterless_sub_dependant( - *, depends: params.Depends, path: str -) -> Dependant: +def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant: assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" ) @@ -299,7 +297,7 @@ def get_dependant( ): raise FastAPIError( f'Dependency parameter "{param_name}" must have a type annotation. ' - f'For example: `{param_name}: SomeType = Depends(...)`' + f"For example: `{param_name}: SomeType = Depends(...)`" ) if ( (dependant.is_gen_callable or dependant.is_async_gen_callable) @@ -339,7 +337,7 @@ def get_dependant( if enforce_annotation and param_details.field is None: raise FastAPIError( f'Dependency parameter "{param_name}" must have a type annotation. ' - f'For example: `{param_name}: SomeType = Depends(...)`' + f"For example: `{param_name}: SomeType = Depends(...)`" ) if isinstance(param_details.field.field_info, params.Body): dependant.body_params.append(param_details.field) diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index 310cf51a50..fbbcacbc6b 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -399,9 +399,11 @@ def test_missing_type_annotation_dependency(): return "db" with pytest.raises(FastAPIError) as exc: + @local_app.get("/bad") def bad(dep=Depends(get_db)): return dep + TestClient(local_app) msg = str(exc.value)